2012年1月29日日曜日

TextBlock にClipboardへのコピー機能を追加する(改訂版)

前回投稿した「TextBlock にClipboardへのコピー機能を追加する」ですが、
あのコードだと、メモリーは消費するわ、メモリーリークしまくるわで使えないことがわかりました。
ここに、改良版を投稿します
  1. メモリーリークの問題
    右クリックで表示しているContextMenuですが、その右クリックを監視している ContextMenuService自体がメモリーリークを起こしているみたいで、OwnerのTextblockが破棄されれもContextMenuServiceに設定されたContextMenuは残っているみたいです。
  2. メモリーの消費
    コードを読めばわかると思いますが、TextBlockの配下にContextMenuServiceとContextMenu、その下にMenuItemが作成されます。これらの生成されたインスタンス「TextBlockClipBoardService.ID=""」によって、すべてのTextBlockの子となるので、メモリーの消費量がタダモンではありません。

    (実際、設定されたTextBlockをDataGridのCellTempleteに設定したらスクロールが耐えられないほどカクカク)

対応策として

  1. なるべく生成するインスタンスを少なくする
    ContextMenuService は諦め、シングルトンのPopupオブジェクトを表示する
  2. メモリーリークをなくす
    循環参照を撲滅する。
観点に再作成しました。



で、できたコードは次の通り
MainPage.xaml
<UserControl  x:Class="TextBlockClipboard.MainPage"
             xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk" 
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:my="clr-namespace:TextBlockClipboard"
             mc:Ignorable="d"
             d:DesignHeight="200" d:DesignWidth="200"        >

    <Grid>
        <StackPanel>
            <TextBlock Text="TextBlock123456" my:TextBlockClipBoardService.ID=""/>
            <TextBlock Text="あああああああああ" my:TextBlockClipBoardService.ID="" />
        </StackPanel>
    </Grid>
</UserControl>

TextBlockClipboard.cs
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Media.Effects;
using System.Windows.Media.Imaging;

namespace TextBlockClipboard
{

    /// <summary>
    /// TextBlock の Text 文字列を Clipboard へ コピーするサービス
    /// </summary>
    public static class TextBlockClipBoardService
    {
        /// <summary>
        /// ID 添付プロパティ
        /// </summary>
        public static readonly DependencyProperty IDProperty =
            DependencyProperty.RegisterAttached("ID",
                                                typeof(string),
                                                typeof(TextBlockClipBoardService),
                                                new PropertyMetadata(null, IDPropertyChanged));
        /// <summary>
        /// ID設定
        /// </summary>
        public static void SetID(DependencyObject obj, string value)
        {
            obj.SetValue(IDProperty, value);
        }
        /// <summary>
        /// ID取得
        /// </summary>
        public static string GetID (DependencyObject obj)
        {
            return (string)obj.GetValue(IDProperty);
        }

        /// <summary>
        /// ポップアップ
        /// </summary>
        private static Popup RightPopupMenu = null;
        /// <summary>
        /// ポップアップに表示するTextBlock
        /// </summary>
        private static TextBlock PopupTextBlock = null;

        /// <summary>
        /// Popup 作成
        /// </summary>
        /// <returns>Popup</returns>
        private static Popup CreatePopupMenu()
        {
            PopupTextBlock = new TextBlock();
            PopupTextBlock.FontSize = 12;
            PopupTextBlock.Padding = new Thickness(5, 2, 5, 2);

            var border = new Border();
            border.BorderThickness = new Thickness(1);
            border.CornerRadius = new CornerRadius(5);
            border.BorderBrush = new SolidColorBrush(Colors.Black);
            border.Background = new SolidColorBrush(SystemColors.MenuColor);
            border.Child = PopupTextBlock;

            var grid = new Grid();
            grid.Effect = new DropShadowEffect { Opacity = 0.4 };
            grid.Children.Add(border);
            var popup = new Popup();
            popup.Child = grid;

            popup.Opened += (s, e) =>
                {
                    Popup localPopup = (Popup)s;
                    var text = (string)localPopup.Tag;
                    if (text.Length > 30)
                    {
                        text = text.Substring(0, 30) + " ... ";
                    }
                    PopupTextBlock.Text = "「" + text + "」をコピーします";
                };
            PopupTextBlock.MouseLeftButtonUp += (s, e) =>
                {
                    try
                    {
                        Clipboard.SetText((string)popup.Tag);
                    }
                    catch (System.Security.SecurityException)
                    {
                    }
                    popup.IsOpen = false;
                    e.Handled = true;
                };
            PopupTextBlock.MouseLeave += (s, e) =>
                {
                    popup.IsOpen = false;
                };

            return popup;
        }
        /// <summary>
        /// ID設定(TextBlockに仕掛けを設定)
        /// </summary>
        /// <param name="d"></param>
        /// <param name="e"></param>
        private static void IDPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var uiTextBlock = d as TextBlock;
            if (uiTextBlock != null)
            {
                if (null == RightPopupMenu)
                {
                    RightPopupMenu = CreatePopupMenu();
                }
                uiTextBlock.MouseRightButtonDown += (sender, ea) =>
                    {
                        RightPopupMenu.IsOpen = false;
                        ea.Handled = true;
                    };
                uiTextBlock.MouseRightButtonUp += (sender,ea) =>
                    {
                        // 表示位置はカーソルがPopupの中へ入るように
                        RightPopupMenu.HorizontalOffset = ea.GetPosition(null).X - 50.0;
                        RightPopupMenu.VerticalOffset = ea.GetPosition(null).Y - 5.0;

                        TextBlock tb = (TextBlock)sender;
                        RightPopupMenu.Tag = tb.Text;
                        RightPopupMenu.IsOpen = true;
                        ea.Handled = true;
                    };
                uiTextBlock.MouseLeftButtonUp += (sender,ea) =>
                    {
                        RightPopupMenu.IsOpen = false;
                    };
            }
            else
            {
                throw new InvalidProgramException("TextBlockClipBoardService は TextBlock のみサポートしてます");
            }
        }
    }
}

画面はこんな感じ


プロジェクト:TextBlockClipboard2.zip

0 件のコメント:

コメントを投稿