OSDN Git Service

OTPictureBox.SetImageFromTaskメソッドで無視する対象の例外にIOExceptionを追加 (thx @sobachanko!)
[opentween/open-tween.git] / OpenTween / OTPictureBox.cs
index d366023..2e9c290 100644 (file)
@@ -22,6 +22,7 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Runtime.InteropServices;
 using System.Text;
 using System.Windows.Forms;
 using System.ComponentModel;
@@ -29,241 +30,155 @@ using System.Drawing;
 using System.Threading.Tasks;
 using System.Threading;
 using System.Net;
+using System.Net.Http;
 using System.IO;
+using OpenTween.Thumbnail;
 
 namespace OpenTween
 {
     public class OTPictureBox : PictureBox
     {
-        [Localizable(true)]
-        public new Image Image
+        [Browsable(false)]
+        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+        public new MemoryImage Image
         {
-            get { return base.Image; }
+            get { return this.memoryImage; }
             set
             {
-                if (this.memoryImage != null)
-                {
-                    this.memoryImage.Dispose();
-                    this.memoryImage = null;
-                }
-                base.Image = value;
+                this.memoryImage = value;
+                base.Image = value?.Image;
+
+                this.RestoreSizeMode();
             }
         }
+        private MemoryImage memoryImage;
 
         [Localizable(true)]
-        public new string ImageLocation
+        [DefaultValue(PictureBoxSizeMode.Normal)]
+        public new PictureBoxSizeMode SizeMode
         {
-            get { return this._ImageLocation; }
+            get { return this.currentSizeMode; }
             set
             {
-                if (value == null)
+                this.currentSizeMode = value;
+
+                if (base.Image != base.InitialImage && base.Image != base.ErrorImage)
                 {
-                    this.Image = null;
-                    return;
+                    base.SizeMode = value;
                 }
-                this.LoadAsync(value);
             }
         }
-        private string _ImageLocation;
 
         /// <summary>
-        /// ImageLocation によりロードされた画像
+        /// InitialImage や ErrorImage の表示に SizeMode を一時的に変更するため、
+        /// 現在の SizeMode を記憶しておくためのフィールド
         /// </summary>
-        private MemoryImage memoryImage = null;
-
-        private Task loadAsyncTask = null;
-        private CancellationTokenSource loadAsyncCancelTokenSource = null;
+        private PictureBoxSizeMode currentSizeMode;
 
-        public new Task LoadAsync(string url)
+        public void ShowInitialImage()
         {
-            this._ImageLocation = url;
-
-            if (this.loadAsyncTask != null && !this.loadAsyncTask.IsCompleted)
-                this.CancelAsync();
-
-            if (this.expandedInitialImage != null)
-                this.Image = this.expandedInitialImage;
+            base.Image = base.InitialImage;
 
-            Uri uri;
-            try
-            {
-                uri = new Uri(url);
-            }
-            catch (UriFormatException)
-            {
-                uri = new Uri(Path.GetFullPath(url));
-            }
-
-            var client = new OTWebClient();
-
-            client.DownloadProgressChanged += (s, e) =>
-            {
-                this.OnLoadProgressChanged(new ProgressChangedEventArgs(e.ProgressPercentage, e.UserState));
-            };
-
-            this.loadAsyncCancelTokenSource = new CancellationTokenSource();
-            var cancelToken = this.loadAsyncCancelTokenSource.Token;
-            var loadImageTask = client.DownloadDataAsync(uri, cancelToken);
-
-            // UnobservedTaskException イベントを発生させないようにする
-            loadImageTask.ContinueWith(t => { var ignore = t.Exception; }, TaskContinuationOptions.OnlyOnFaulted);
-
-            var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
-
-            return loadImageTask.ContinueWith(t => {
-                if (t.IsFaulted) throw t.Exception;
+            // InitialImage は SizeMode の値に依らず中央等倍に表示する必要がある
+            base.SizeMode = PictureBoxSizeMode.CenterImage;
+        }
 
-                return MemoryImage.CopyFromBytes(t.Result);
-            }, cancelToken)
-            .ContinueWith(t =>
-            {
-                if (!t.IsCanceled)
-                {
-                    if (t.IsFaulted)
-                    {
-                        this.Image = this.expandedErrorImage;
-                    }
-                    else
-                    {
-                        this.Image = t.Result.Image;
-                        this.memoryImage = t.Result;
-                    }
-                }
+        public void ShowErrorImage()
+        {
+            base.Image = base.ErrorImage;
 
-                var exp = t.Exception != null ? t.Exception.Flatten() : null;
-                this.OnLoadCompleted(new AsyncCompletedEventArgs(exp, t.IsCanceled, null));
-            }, uiScheduler);
+            // ErrorImage は SizeMode の値に依らず中央等倍に表示する必要がある
+            base.SizeMode = PictureBoxSizeMode.CenterImage;
         }
 
-        public new void CancelAsync()
+        private void RestoreSizeMode()
         {
-            if (this.loadAsyncTask == null || this.loadAsyncTask.IsCompleted)
-                return;
+            base.SizeMode = this.currentSizeMode;
+        }
+
+        /// <summary>
+        /// SetImageFromTask メソッドを連続で呼び出した際に設定される画像が前後するのを防ぐため、
+        /// 現在進行中の Task を表す Id を記憶しておくためのフィールド
+        /// </summary>
+        private int currentImageTaskId = 0;
 
-            this.loadAsyncCancelTokenSource.Cancel();
+        public async Task SetImageFromTask(Func<Task<MemoryImage>> imageTask)
+        {
+            var id = Interlocked.Increment(ref this.currentImageTaskId);
 
             try
             {
-                this.loadAsyncTask.Wait();
+                this.ShowInitialImage();
+
+                var image = await imageTask();
+
+                if (id == this.currentImageTaskId)
+                    this.Image = image;
             }
-            catch (AggregateException ae)
+            catch (Exception)
             {
-                ae.Handle(e =>
+                if (id == this.currentImageTaskId)
+                    this.ShowErrorImage();
+                try
                 {
-                    if (e is OperationCanceledException)
-                        return true;
-                    if (e is WebException)
-                        return true;
-
-                    return false;
-                });
+                    throw;
+                }
+                catch (HttpRequestException) { }
+                catch (InvalidImageException) { }
+                catch (OperationCanceledException) { }
+                catch (WebException) { }
+                catch (IOException) { }
             }
         }
 
-        public new Image ErrorImage
+        protected override void OnPaint(PaintEventArgs pe)
         {
-            get { return base.ErrorImage; }
-            set
+            try
             {
-                base.ErrorImage = value;
-                this.UpdateStatusImages();
-            }
-        }
+                base.OnPaint(pe);
 
-        public new Image InitialImage
-        {
-            get { return base.InitialImage; }
-            set
-            {
-                base.InitialImage = value;
-                this.UpdateStatusImages();
+                // 動画なら再生ボタンを上から描画
+                DrawPlayableMark(pe);
             }
-        }
-
-        private Image expandedErrorImage = null;
-        private Image expandedInitialImage = null;
-
-        /// <summary>
-        /// ErrorImage と InitialImage の表示用の画像を生成する
-        /// </summary>
-        /// <remarks>
-        /// ErrorImage と InitialImage は SizeMode の値に依らず中央等倍に表示する必要があるため、
-        /// 事前にコントロールのサイズに合わせた画像を生成しておく
-        /// </remarks>
-        private void UpdateStatusImages()
-        {
-            var isError = false;
-            var isInit = false;
-
-            if (this.Image != null)
+            catch (ExternalException)
             {
-                // ErrorImage か InitialImage を使用中であれば記憶しておく
-                isError = (this.Image == this.expandedErrorImage);
-                isInit = (this.Image == this.expandedInitialImage);
+                // アニメーション GIF 再生中に発生するエラーの対策
+                // 参照: https://sourceforge.jp/ticket/browse.php?group_id=6526&tid=32894
+                this.ShowErrorImage();
             }
-
-            if (isError || isInit)
-                this.Image = null;
-
-            if (this.expandedErrorImage != null)
-                this.expandedErrorImage.Dispose();
-
-            if (this.expandedInitialImage != null)
-                this.expandedInitialImage.Dispose();
-
-            this.expandedErrorImage = this.ExpandImage(this.ErrorImage);
-            this.expandedInitialImage = this.ExpandImage(this.InitialImage);
-
-            if (isError)
-                this.Image = this.expandedErrorImage;
-
-            if (isInit)
-                this.Image = this.expandedInitialImage;
         }
 
-        private Image ExpandImage(Image image)
+        private void DrawPlayableMark(PaintEventArgs pe)
         {
-            if (image == null) return null;
-
-            var bitmap = new Bitmap(this.ClientSize.Width, this.ClientSize.Height);
-
-            using (var g = this.CreateGraphics())
-            {
-                bitmap.SetResolution(g.DpiX, g.DpiY);
-            }
+            var thumb = this.Tag as ThumbnailInfo;
+            if (thumb == null || !thumb.IsPlayable) return;
+            if (base.Image == base.InitialImage || base.Image == base.ErrorImage) return;
 
-            using (var g = Graphics.FromImage(bitmap))
-            {
-                var posx = (bitmap.Width - image.Width) / 2;
-                var posy = (bitmap.Height - image.Height) / 2;
+            var overlayImage = Properties.Resources.PlayableOverlayImage;
 
-                g.DrawImage(image,
-                    new Rectangle(posx, posy, image.Width, image.Height),
-                    new Rectangle(0, 0, image.Width, image.Height),
-                    GraphicsUnit.Pixel);
-            }
+            var overlaySize = Math.Min(this.Width, this.Height) / 4;
+            var destRect = new Rectangle(
+                (this.Width - overlaySize) / 2,
+                (this.Height - overlaySize) / 2,
+                overlaySize,
+                overlaySize);
 
-            return bitmap;
+            pe.Graphics.DrawImage(overlayImage, destRect, 0, 0, overlayImage.Width, overlayImage.Height, GraphicsUnit.Pixel);
         }
 
-        protected override void OnResize(EventArgs e)
+        [Browsable(false)]
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+        public new string ImageLocation
         {
-            base.OnResize(e);
-            this.UpdateStatusImages();
+            get { return null; }
+            set { }
         }
 
-        protected override void Dispose(bool disposing)
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        public new void Load(string url)
         {
-            base.Dispose(disposing);
-
-            if (this.expandedErrorImage != null)
-                this.expandedErrorImage.Dispose();
-
-            if (this.expandedInitialImage != null)
-                this.expandedInitialImage.Dispose();
-
-            if (this.memoryImage != null)
-                this.memoryImage.Dispose();
+            throw new NotSupportedException();
         }
     }
 }