Ktouth Brand. on Web

け〜くんこと K.Ktouth のだらだらした日常と突発的に作るプログラムや読み物とかの雑多サイト



[2012年03月04日]

とりあえず完成: TrimmingPath ヘルパークラス

2012年03月05日 02:37更新 筆者:K.Ktouth

この辺から始まったこの辺までのいわゆる「パス文字列のトリミングと、それを行ってくれる TextBlock」の実装をやっていたんですが。
最終的に TextBlock にアタッチする依存関係プロパティの集合という形に落ち着きました。この方法ならDependencyObject の落とし穴も関係ないし。
使用方法としては以下の通り。

<TextBlock xmlns:kcontrol="clr-namespace:KtouthBrand.WPF.Controls"
      kcontrol:TrimmingPath.Source="C:\Users\foobar\document\sources\適当なパス文字列.cs" />

名前空間指定をして依存関係でソースとなるパス文字列を指定するだけ。これで Text プロパティにトリミングした文字列が、 Tooltip には必要に応じて元のパス文字列が設定されます。もちろんフォントサイズやスタイル、TextBlockの要素幅の変更に追随します。
Tooltip が必要ない場合は kcontrol:TrimmingPath.AutoTooltip="false" を指定します。ツールチップは別の要素に指定したいと言う時は kcontrol:TrimmingPath.TooltipTarget="{Binding ElementName=barbaz}" のように指定します。
本当ならきちんとツールチップのスタイルテンプレート適用とかコンバータに対応した方がいいのかも知れないけど、その辺はまぁマニュアルでやって下さいと言うことで。静的メソッドも準備してあるし。

ふぅ、ようやくこれで本来のアプリ実装に入れます(ぉぃ

(以下、TrimmingPath の実装コード)

ソース

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;

namespace KtouthBrand.WPF.Controls
{
  /// <summary>パス文字列を <see cref="TextBlock"/> の ActualWidth プロパティにあわせてトリミング処理を行う機能を提供するヘルパークラス</summary>
  public static class TrimmingPath
  {
    #region 静的メソッド

    [DllImport("shlwapi.dll", CharSet = CharSet.Auto)]
    private static extern bool PathCompactPathEx([Out] StringBuilder pszOut, string szPath, int cchMax, int dwFlags);

    /// <summary>文字数にあわせてパス文字列をトリミングします</summary>
    /// <param name="path">パス文字列</param>
    /// <param name="length">最大文字数</param>
    /// <returns>トリミングを行ったパス文字列</returns>
    public static string Trim(string path, int length)
    {
      StringBuilder sb = new StringBuilder(length + 1);
      PathCompactPathEx(sb, path, length + 1, 0);
      return sb.ToString();
    }

    /// <summary>要素幅とフォント情報からパス文字列をトリミングします</summary>
    /// <param name="path">パス文字列</param>
    /// <param name="width">最大要素幅。ピクセル数で指定します</param>
    /// <param name="typeFace">描画に使用されるフォント情報</param>
    /// <param name="fontSize">描画に使用されるフォントサイズ</param>
    /// <param name="culture">カルチャー情報</param>
    /// <returns>トリミングを行ったパス文字列</returns>
    /// <remarks>
    /// 要素幅が0.0以下の時もしくはトリミングの必要が無い時はトリミングを行いません。<br />
    /// また、トリミングしても要素幅に収められない場合は最もトリミングした文字列を返します。
    /// </remarks>
    public static string Trim(string path, double width, Typeface typeFace, double fontSize, CultureInfo culture)
    {
      var result = path;
        try
        {
          for (var len = path.Length - 1; len > 0; len--)
          {
            var formattedText = new FormattedText(result, culture, FlowDirection.LeftToRight, typeFace, fontSize, Brushes.Black);
            if (formattedText.WidthIncludingTrailingWhitespace <= width) break;
            var str = Trim(path, len);
            if (result == str) break;
            result = str;
          }
        }
        catch { }
      return result;
    }

    /// <summary>要素幅とフォント情報からパス文字列をトリミングします</summary>
    /// <param name="path">パス文字列</param>
    /// <param name="width">最大要素幅。ピクセル数で指定します</param>
    /// <param name="fontFamily">描画に使用されるフォントのファミリー名称</param>
    /// <param name="fontSize">描画に使用されるフォントサイズ</param>
    /// <param name="culture">カルチャー情報</param>
    /// <returns>トリミングを行ったパス文字列</returns>
    /// <remarks>
    /// 要素幅が0.0以下の時もしくはトリミングの必要が無い時はトリミングを行いません。<br />
    /// また、トリミングしても要素幅に収められない場合は最もトリミングした文字列を返します。
    /// </remarks>
    public static string Trim(string path, double width, string fontFamily, double fontSize, CultureInfo culture) { return Trim(path, width, new Typeface(fontFamily), fontSize, culture); }

    /// <summary>要素幅とフォント情報からパス文字列をトリミングします</summary>
    /// <param name="path">パス文字列</param>
    /// <param name="width">最大要素幅。ピクセル数で指定します</param>
    /// <param name="fontFamily">描画に使用されるフォントのファミリー名称</param>
    /// <param name="fontSize">描画に使用されるフォントサイズ</param>
    /// <returns>トリミングを行ったパス文字列</returns>
    /// <remarks>
    /// 要素幅が0.0以下の時もしくはトリミングの必要が無い時はトリミングを行いません。<br />
    /// また、トリミングしても要素幅に収められない場合は最もトリミングした文字列を返します。
    /// </remarks>
    public static string Trim(string path, double width, string fontFamily, double fontSize) { return Trim(path, width, fontFamily, fontSize, CultureInfo.CurrentUICulture); }

    #endregion

    #region 添付依存関係プロパティ

    /// <summary>トリミングして表示するパス文字列を取得します。このプロパティはアタッチされた依存関係プロパティです。</summary>
    /// <param name="obj">アタッチする <see cref="TextBlock"/></param>
    /// <returns>トリミング前のパス文字列</returns>
    public static string GetSource(TextBlock obj)
    {
      return (string)obj.GetValue(SourceProperty);
    }

    /// <summary>トリミングして表示するパス文字列を設定します。このプロパティはアタッチされた依存関係プロパティです。</summary>
    /// <param name="obj">アタッチする <see cref="TextBlock"/></param>
    /// <param name="value">トリミング前のパス文字列</param>
    public static void SetSource(TextBlock obj, string value)
    {
      obj.SetValue(SourceProperty, value);
    }

    /// <summary>トリミングして表示するパス文字列の取得または設定を行うアタッチされた依存関係プロパティのメタ情報です。</summary>
    public static readonly DependencyProperty SourceProperty =
      DependencyProperty.RegisterAttached("Source", typeof(string), typeof(TrimmingPath),
                        new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender, SourcePropertyChanged));
    private static void SourcePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
      var textBlock = sender as TextBlock;
      if (textBlock != null)
      {
        var watcher = PropertiesWatcher.GetWacther(textBlock) ?? new PropertiesWatcher(textBlock);
        watcher.ResetTrimmedSource();
      }
    }

    /// <summary>パス文字列がトリミングされている際にツールチップにフルパスを指定するかを取得します。このプロパティはアタッチされた依存関係プロパティです。</summary>
    /// <param name="obj">アタッチする <see cref="TextBlock"/></param>
    /// <returns>フルパスを設定するなら true。規定値は true</returns>
    public static bool GetAutoTooltip(TextBlock obj)
    {
      return (bool)obj.GetValue(AutoTooltipProperty);
    }

    /// <summary>パス文字列がトリミングされている際にツールチップにフルパスを指定するかを設定します。このプロパティはアタッチされた依存関係プロパティです。</summary>
    /// <param name="obj">アタッチする <see cref="TextBlock"/></param>
    /// <param name="value">フルパスを設定するなら true。規定値は true</param>
    public static void SetAutoTooltip(TextBlock obj, bool value)
    {
      obj.SetValue(AutoTooltipProperty, value);
    }

    /// <summary>パス文字列がトリミングされている際にツールチップにフルパスを指定するか取得または設定を行うアタッチされた依存関係プロパティのメタ情報です。</summary>
    public static readonly DependencyProperty AutoTooltipProperty =
      DependencyProperty.RegisterAttached("AutoTooltip", typeof(bool), typeof(TrimmingPath),
                        new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.AffectsRender, AutoTooltipPropertyChanged));
    private static void AutoTooltipPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
      var textBlock = sender as TextBlock;
      if (textBlock != null)
      {
        RemoveTooltip(textBlock, GetTooltipTarget(textBlock));
        var watcher = PropertiesWatcher.GetWacther(textBlock) ?? new PropertiesWatcher(textBlock);
        watcher.ResetTooltip();
      }
    }

    /// <summary>ツールチップ表示の親となる <see cref="FrameworkElement"/> を取得します。このプロパティはアタッチされた依存関係プロパティです。</summary>
    /// <param name="obj">アタッチする <see cref="TextBlock"/></param>
    /// <returns>対象となる <see cref="FrameworkElement"/></returns>
    /// <remarks>null が指定されている場合、このプロパティがアタッチされた <see cref="TextBlock"/>が対象になります</remarks>
    public static FrameworkElement GetTooltipTarget(TextBlock obj)
    {
      return (FrameworkElement)obj.GetValue(TooltipTargetProperty);
    }

    /// <summary>ツールチップ表示の親となる <see cref="FrameworkElement"/> を設定します。このプロパティはアタッチされた依存関係プロパティです。</summary>
    /// <param name="obj">アタッチする <see cref="TextBlock"/></param>
    /// <param name="value">対象となる <see cref="FrameworkElement"/></param>
    /// <remarks>null が指定されている場合、このプロパティがアタッチされた <see cref="TextBlock"/>が対象になります</remarks>
    public static void SetTooltipTarget(TextBlock obj, FrameworkElement value)
    {
      obj.SetValue(TooltipTargetProperty, value);
    }

    /// <summary>ツールチップ表示の親となる <see cref="FrameworkElement"/> の取得または設定を行うアタッチされた依存関係プロパティのメタ情報です。</summary>
    public static readonly DependencyProperty TooltipTargetProperty =
      DependencyProperty.RegisterAttached("TooltipTarget", typeof(FrameworkElement), typeof(TrimmingPath),
                        new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender, TooltipTargetPropertyChanged));
    private static void TooltipTargetPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
      var textBlock = sender as TextBlock;
      if (textBlock != null)
      {
        RemoveTooltip(textBlock, e.OldValue as FrameworkElement);
        var watcher = PropertiesWatcher.GetWacther(textBlock) ?? new PropertiesWatcher(textBlock);
        watcher.ResetTooltip();
      }
    }
    private static void RemoveTooltip(TextBlock target, FrameworkElement element)
    {
      if (element != null && !BindingOperations.IsDataBound(element, FrameworkElement.ToolTipProperty)) { element.ToolTip = null; }
      if (target != element && !BindingOperations.IsDataBound(target, FrameworkElement.ToolTipProperty)) { target.ToolTip = null; }
    }

    #endregion

    #region プロパティ監視用クラス

    private sealed class PropertiesWatcher : DependencyObject
    {
      private static readonly List<WeakReference> Watchers = new List<WeakReference>();
      public static PropertiesWatcher GetWacther(TextBlock target)
      {
        if(target == null) { return null; }
        var no_trim = true;
        try
        {
          foreach (var i in Watchers)
          {
            var x = i.Target as PropertiesWatcher;
            TextBlock tb = (x != null) ? x.Target : null;
            no_trim = no_trim && tb != null;
            if (tb == target) { return x; }
          }
          return null;
        }
        finally
        {
          if (!no_trim) { Watchers.RemoveAll(x => !x.IsAlive); }
        }
      }

      public PropertiesWatcher(TextBlock target)
        : base()
      {
        Target = target;
        Watchers.Add(new WeakReference(this));
      }

      public double FontSize
      {
        get { return (double)GetValue(FontSizeProperty); }
        set { SetValue(FontSizeProperty, value); }
      }
      public static readonly DependencyProperty FontSizeProperty =
        DependencyProperty.Register("FontSize", typeof(double), typeof(PropertiesWatcher), new UIPropertyMetadata(Double.NaN));

      public FontFamily FontFamily
      {
        get { return (FontFamily)GetValue(FontFamilyProperty); }
        set { SetValue(FontFamilyProperty, value); }
      }
      public static readonly DependencyProperty FontFamilyProperty =
        DependencyProperty.Register("FontFamily", typeof(FontFamily), typeof(PropertiesWatcher), new UIPropertyMetadata(null));

      public FontStretch FontStretch
      {
        get { return (FontStretch)GetValue(FontStretchProperty); }
        set { SetValue(FontStretchProperty, value); }
      }
      public static readonly DependencyProperty FontStretchProperty =
        DependencyProperty.Register("FontStretch", typeof(FontStretch), typeof(PropertiesWatcher), new UIPropertyMetadata(FontStretches.Normal));

      public FontWeight FontWeight
      {
        get { return (FontWeight)GetValue(FontWeightProperty); }
        set { SetValue(FontWeightProperty, value); }
      }
      public static readonly DependencyProperty FontWeightProperty =
        DependencyProperty.Register("FontWeight", typeof(FontWeight), typeof(PropertiesWatcher), new UIPropertyMetadata(FontWeights.Normal));

      public FontStyle FontStyle
      {
        get { return (FontStyle)GetValue(FontStyleProperty); }
        set { SetValue(FontStyleProperty, value); }
      }

      public static readonly DependencyProperty FontStyleProperty =
        DependencyProperty.Register("FontStyle", typeof(FontStyle), typeof(PropertiesWatcher), new UIPropertyMetadata(FontStyles.Normal));

      private static readonly DependencyProperty[] WatchProperties = new DependencyProperty[] {
        FontSizeProperty, FontFamilyProperty, FontStretchProperty, FontWeightProperty, FontStyleProperty,
      };

      public TextBlock Target
      {
        get
        {
          var bind = BindingOperations.GetBinding(this, PropertiesWatcher.FontSizeProperty);
          return bind != null ? bind.Source as TextBlock : null;
        }
        private set
        {
          if (Target != value)
          {
            _IsInitializing = true;
            try
            {
              BindingOperations.ClearAllBindings(this);
              if (Target != null) { Target.SizeChanged -= TargetSizeChanged; }
              if (value != null)
              {
                foreach (var dp in WatchProperties)
                {
                  BindingOperations.SetBinding(this, dp, new Binding(dp.Name) { Source = value, Mode = BindingMode.OneWay });
                }
                Target.SizeChanged += TargetSizeChanged;

                ResetTypeface();
              }
            }
            finally { _IsInitializing = false; }
          }
        }
      }
      private bool _IsInitializing = true;

      protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
      {
        base.OnPropertyChanged(e);
        if (!_IsInitializing)
        {
          if (e.Property != FontSizeProperty) { ResetTypeface(); }
          if (e.Property == FontSizeProperty) { ResetTrimmedSource(); }
        }
      }
      private void ResetTypeface()
      {
        try { _Typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); }
        catch { _Typeface = null; }
        if (_Typeface != null) { ResetTrimmedSource(); }
      }
      private Typeface _Typeface = null;

      private void TargetSizeChanged(object sender, SizeChangedEventArgs e) { ResetTrimmedSource(); }
      public void ResetTrimmedSource()
      {
        var target = Target;
        if (target != null)
        {
          if (!BindingOperations.IsDataBound(target, TextBlock.TextProperty))
          {
            target.Text = Trim(GetSource(target), target.ActualWidth, _Typeface, FontSize, CultureInfo.CurrentUICulture);
          }
          ResetTooltip();
        }
      }
      public void ResetTooltip()
      {
        var target = Target;
        if (target != null)
        {
          var tooltip = GetTooltipTarget(target) ?? target;
          if (!BindingOperations.IsDataBound(tooltip, FrameworkElement.ToolTipProperty))
          {
            var source = GetSource(target);
            tooltip.ToolTip = source != target.Text ? source : null;
          }
        }
      }
    }

    #endregion
  }
}

本日のリンク元
その他のリンク元
検索