20210115のC#に関する記事は3件です。

MahAppsのHamburgerMenuでMaterialDesignのアイコンをコードからセットする

はじめに

Windows Template StudioからWPFアプリを作成したときに、自動的にMahAppsのHamburgerMenuが作成されるのですが、HamburgerMenuにMaterialDesignのアイコンを利用したいなと思いました。
いろいろ試した結果うまく動作したので覚書も含めて書いておこうと思います。

DataTemplateの作成

IconDataTemplate を作成します
一緒に最初から書かれているHamburgerMenuGlyphItemも参考に併記しておきます

    public class MenuItemTemplateSelector : DataTemplateSelector
    {
        public DataTemplate GlyphDataTemplate { get; set; }

        public DataTemplate IconDataTemplate { get; set; }

        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            if (item is HamburgerMenuGlyphItem)
            {
                return GlyphDataTemplate;
            }

             if (item is HamburgerMenuIconItem)
            {
                return IconDataTemplate;
            }


            return base.SelectTemplate(item, container);
        }
    }

Viewのxaml

MenuItemTemplateSelector.IconDataTemplateのみ書いています。
MenuItemTemplateSelector.GlyphDataTemplateはデフォルトのままなので記載していません

xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"

<controls:MetroWindow.Resources>
    <templateSelectors:MenuItemTemplateSelector x:Key="MenuItemTemplateSelector">
        <templateSelectors:MenuItemTemplateSelector.IconDataTemplate>
            <DataTemplate DataType="{x:Type controls:HamburgerMenuIconItem}">
                <Grid Height="48">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="48" />
                        <ColumnDefinition />
                    </Grid.ColumnDefinitions>
                    <materialDesign:PackIcon Grid.Column="0"
                                    Kind="{Binding Icon}"
                                    VerticalAlignment="Center"
                                    HorizontalAlignment="Center"
                                    Width="32"
                                    Height="32"/>
                    <TextBlock
                        Grid.Column="1"
                        VerticalAlignment="Center"
                        FontSize="16"
                        Text="{Binding Label}" />
                </Grid>
            </DataTemplate>
        </templateSelectors:MenuItemTemplateSelector.IconDataTemplate>
    </templateSelectors:MenuItemTemplateSelector>
</controls:MetroWindow.Resources>

ViewModel

using MahApps.Metro.Controls;
using MaterialDesignThemes.Wpf;

  public ObservableCollection<HamburgerMenuItem> MenuItems { get; } = new ObservableCollection<HamburgerMenuItem>()
  {
      new HamburgerMenuIconItem(){Label = Resources.ShellStartPage,
                                  Icon = PackIconKind.Home,
                                  Tag = PageKeys.Start },

      new HamburgerMenuGlyphItem() { Label = Resources.ShellSettingsPage, 
                                     Glyph = "\uE8A5", 
                                     Tag = PageKeys.Settings },
  };

実行

こんな感じでうまく表示できました
image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MahAppsのHamburgerMenuでMaterialDesignのアイコンをViewModelからバインドする

はじめに

Windows Template StudioからWPFアプリを作成したときに、自動的にMahAppsのHamburgerMenuが作成されるのですが、HamburgerMenuにMaterialDesignのアイコンを利用したいなと思いました。
いろいろ試した結果うまく動作したので覚書も含めて書いておこうと思います。

DataTemplateの作成

IconDataTemplate を作成します
一緒に最初から書かれているHamburgerMenuGlyphItemも参考に併記しておきます

    public class MenuItemTemplateSelector : DataTemplateSelector
    {
        public DataTemplate GlyphDataTemplate { get; set; }

        public DataTemplate IconDataTemplate { get; set; }

        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            if (item is HamburgerMenuGlyphItem)
            {
                return GlyphDataTemplate;
            }

             if (item is HamburgerMenuIconItem)
            {
                return IconDataTemplate;
            }


            return base.SelectTemplate(item, container);
        }
    }

Viewのxaml

MenuItemTemplateSelector.IconDataTemplateのみ書いています。
MenuItemTemplateSelector.GlyphDataTemplateはデフォルトのままなので記載していません

xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"

<controls:MetroWindow.Resources>
    <templateSelectors:MenuItemTemplateSelector x:Key="MenuItemTemplateSelector">
        <templateSelectors:MenuItemTemplateSelector.IconDataTemplate>
            <DataTemplate DataType="{x:Type controls:HamburgerMenuIconItem}">
                <Grid Height="48">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="48" />
                        <ColumnDefinition />
                    </Grid.ColumnDefinitions>
                    <materialDesign:PackIcon Grid.Column="0"
                                    Kind="{Binding Icon}"
                                    VerticalAlignment="Center"
                                    HorizontalAlignment="Center"
                                    Width="32"
                                    Height="32"/>
                    <TextBlock
                        Grid.Column="1"
                        VerticalAlignment="Center"
                        FontSize="16"
                        Text="{Binding Label}" />
                </Grid>
            </DataTemplate>
        </templateSelectors:MenuItemTemplateSelector.IconDataTemplate>
    </templateSelectors:MenuItemTemplateSelector>
</controls:MetroWindow.Resources>

ViewModel

using MahApps.Metro.Controls;
using MaterialDesignThemes.Wpf;

  public ObservableCollection<HamburgerMenuItem> MenuItems { get; } = new ObservableCollection<HamburgerMenuItem>()
  {
      new HamburgerMenuIconItem(){Label = Resources.ShellStartPage,
                                  Icon = PackIconKind.Home,
                                  Tag = PageKeys.Start },

      new HamburgerMenuGlyphItem() { Label = Resources.ShellSettingsPage, 
                                     Glyph = "\uE8A5", 
                                     Tag = PageKeys.Settings },
  };

実行

こんな感じでうまく表示できました
image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

.NET5.0/C#9.0でオートシェイプ風図形描画ライブラリを作ろう!(Chapter3-1)

※本記事は下記のエントリから始まる連載記事となります。
.NET5.0/C#9.0でオートシェイプ風図形描画ライブラリを作ろう!(Chapter0)

Chapter3 ドラッグ操作で図形のサイズを変更してみよう(その1)

今回は下準備として用意するものが多いので、複数回に分割して書きます。

ソースコード

Capter3の内容は下記ブランチにて実装されています。実装の詳細はこちらをご確認ください。
https://github.com/pierre3/CoreShape/tree/blog/capter3

リサイズハンドルを定義しよう

通常図形のサイズを変更する場合、下図のマークのような"つまみ"を操作すると思います。
まずは、この"つまみ"を表現するクラスを用意するところから始めましょう。
なお、以降ではこの"つまみ"の事を「リサイズハンドル」と呼ぶことにします。

image.png

リサイズハンドルの種別

リサイズハンドルは、その位置によって動作が異なります。また、マウスポインタを重ねた際のカーソルの形状も異ります。

そこで、当たり判定時でヒットした際にマウスポインタが図形のどの部分に位置しているかを示すHitResult列挙型を定義します。

public enum HitResult
{
    None,
    ResizeN,
    ResizeNE,
    ResizeE,
    ResizeSE,
    ResizeS,
    ResizeSW,
    ResizeW,
    ResizeNW,
    Body
}

ResizeXXが各リサイズハンドルの位置に対応しています。
Resizeの後ろのアルファベットは、画面上を北とした際の方角の頭文字を表しています。
(例えば N = North, NE = North East)
image.png

また、Noneはヒットしていない状態、Bodyは図形の本体部分にヒットした場合を示す値です。

ResizeHandleBaseクラス

次に全てのリサイズハンドルのベースとなる抽象クラスResizeHandleBaseを定義します。
リサイズハンドルも描画や当たり判定などを行う必要がありますので、IShapeインターフェースを実装して作成することとします。

基本的にはRectangleShapeと同じように作りますが、リサイズハンドル用にカスタマイズします。

  • HitResultプロパティを追加します。
  • HitTest()メソッドでは、Hitした際に自身のHitResultプロパティの値を返すようにします。

それに伴ってIShapeのHitTestメソッドの戻り値もboolからHitResultに変更します

public interface IShape
{
    public HitResult HitTest(Point p);
}
public abstract class ResizeHandleBase : IShape
{
    public HitResult HitResult {set; protected set;}
    public Rectangle Bounds { get; protected set; }
    //外観は白で塗りつぶし、黒の輪郭(既定値)
    public Stroke? Stroke { get; set; } = new Stroke(Color.Black, 1f);
    public Fill? Fill { get; set; } = new Fill(Color.White);

    protected ResizeHandleBase(Rectangle bounds)
    {
        Bounds = bounds;
    }

    public void Draw(IGraphics g)
    {
        if (Fill is not null)
        {
            g.FillRectangle(Bounds, Fill);
        }
        if (Stroke is not null)
        {
            g.DrawRectangle(Bounds, Stroke);
        }
    }

    public HitResult HitTest(Point p)
    {
        return (Bounds.Left <= p.X && p.X <= Bounds.Right
                && Bounds.Top <= p.Y && p.Y <= Bounds.Bottom)
                ? Type
                : HitResult.None;
    }

    public void Drag(Point oldPointer, Point currentPointer)
    {
        //リサイズハンドルではDragメソッドは使用しない
        throw new NotImplementedException();
    }
}

リサイズ処理の追加

リサイズハンドルではDragメソッドを使いません。その代わりに(そのリサイズハンドルを持つ親の)図形の座標を変更するResizeメソッドを定義します。
また、図形の座標に追従してリサイズハンドルの位置も更新する必要があるため、これを行うメソッドSetLocationメソッドも定義します。

public abstract class ResizeHandleBase : IShape
{
    public abstract Rectangle Resize(Point p, Rectangle parentBounds);
    public abstract void SetLocation(Rectangle parentBounds);
}

基底クラスResizeHandleBaseの定義はこれで完了です。

ResizeHandleN の実装

それでは、ResizeHandleNを例に具体的なハンドルの定義に取り掛かりましょう。

ResizeHanldeBaseを継承し、抽象メソッドResize()メソッドとSetLocation()メソッドをオーバーライドします。

Resize()メソッドのオーバーライド

Redizeメソッドではドラッグ先のマウス座標(p)と親となる図形のBounds(parentBounds)を受け取り、リサイズ後のBounds座標を返します。

ResizeHandleNは Bounds の上辺中央のつまみを表します。
Resizeメソッドでは 上辺の位置がマウスポインタのY座標の位置に移動し、その結果図形の高さも変化します。

public override Rectangle Resize(Point p, Rectangle parentBounds)
{
    return new Rectangle(parentBounds.Left, p.Y, parentBounds.Size.Width, parentBounds.Bottom - p.Y);
}

SetLocation()メソッドのオーバーライド

SetLocationメソッドでは、変更された親図形のBoundsに合わせて自身の位置を再設定します。

ResizeHandleNでは、親となる図形の Bounds の上辺中央に合わせるように設定します。

public override void SetLocation(Rectangle parentBounds)
{
    var center = new Point(parentBounds.Left + parentBounds.Size.Width / 2, parentBounds.Top);
    Bounds = new Rectangle(
        center.X - Bounds.Size.Width / 2,
        center.Y - Bounds.Size.Height / 2,
        Bounds.Size.Width,
        Bounds.Size.Height);
}

他のリサイズハンドルも同様に作成します。

https://github.com/pierre3/CoreShape/tree/blog/capter3/CoreShape/Shapes/ResizeHandles

  • ResizeHandleN
  • ResizeHandleNE
  • ResizeHandleE
  • ResizeHandleSE
  • ResizeHandleS
  • ResizeHandleSW
  • ResizeHandleW
  • ResizeHandleNW

リサイズハンドルをまとめるクラスを作ろう

リサイズハンドルの準備ができましたら、これら8つのハンドルをまとめて処理するためのクラスResizeHandleCollectionを定義します。

各ハンドルはコンストラクタでItemsプロパティにReadOnlyCollection<ResizeHandleBase>として作成します。
コンストラクタでは、(今のところ)ハンドルのサイズのみが指定できるようにしています。

そして、ハンドルを操作する下記の処理を追加します。

  • 親の図形に合わせて自身の位置を更新するSetLocation()メソッドはまとめて実行します。
  • 描画処理(Draw())もまとめて実施します。
  • ヒットテスト(HitTest()) は順番に実行し、ヒットした時点でそのハンドルのHitResultの値を返すようにします。
    また、ヒットしたハンドルの参照をActiveHandleプロパティに保持しておきます。
  • リサイズ処理ではActiveHandleに設定されたハンドルのみResize()メソッドを実行します。
public class ResizeHandleCollection
{
    protected IReadOnlyCollection<ResizeHandleBase> Items { get; set; }

    public ResizeHandleBase? ActiveHandle { get; protected set; }

    public ResizeHandleCollection(float width, float height)
    {
        Items = new ReadOnlyCollection<ResizeHandleBase>(
            new ResizeHandleBase[]
            {
                new ResizeHandleN(new Rectangle(0, 0, width, height)),
                new ResizeHandleNE(new Rectangle(0, 0, width, height)),
                new ResizeHandleE(new Rectangle(0, 0, width, height)),
                new ResizeHandleSE(new Rectangle(0, 0, width, height)),
                new ResizeHandleS(new Rectangle(0, 0, width, height)),
                new ResizeHandleSW(new Rectangle(0, 0, width, height)),
                new ResizeHandleW(new Rectangle(0, 0, width, height)),
                new ResizeHandleNW(new Rectangle(0, 0, width, height))
            });
    }

    public void SetLocation(Rectangle parentBounds)
    {
        foreach (var handle in Items)
        {
            handle.SetLocation(parentBounds);
        }
    }

    public HitResult HitTest(Point p)
    {
        foreach (var handle in Items)
        {
            var hitResult= handle.HitTest(p);
            if (hitResult is not HitResult.None)
            {
                ActiveHandle = handle;
                return hitResult;
            }
        }
        ActiveHandle = null;
        return HitResult.None;
    }

    public Rectangle Resize(Point p, Rectangle parentBounds)
    {
        return ActiveHandle?.Resize(p, parentBounds) ?? parentBounds;
    }

    public void Draw(IGraphics g)
    {
        foreach (var handle in Items)
        {
            handle.Draw(g);
        }
    }
}

RectangleShapeにリサイズハンドルを追加しよう

それではハンドルの準備ができましたので、RectangleShapeのメンバーにResizeHandleColectionを追加して
図形の変形(リサイズ)機能を実装してみましょう。

コンストラクタでハンドルの初期化を行ったら、 Draw()HitTest()Drag()メソッドの処理を下記のように書き換えます。処理の詳細はコード内のコメントを参照してください。

public class RectangleShape : IShape
{
    //...(省略)
    protected ResizeHandleCollection ResizeHandles { get; set; }

    public RectangleShape(Rectangle bounds)
    {
        Bounds = bounds;
        HitTestStrategy = new RectangleHitTestStrategy();
        //ResizeHandleColection作成後、Boundsに合わせて配置
        ResizeHandles = new ResizeHandleCollection(8, 8);
        ResizeHandles.SetLocation(Bounds);
    }

    public RectangleShape(Rectangle bounds, IHitTestStrategy<RectangleShape> hitTestStrategy)
    {
        Bounds = bounds;
        HitTestStrategy = hitTestStrategy;
        //ResizeHandleColection作成後、Boundsに合わせて配置
        ResizeHandles = new ResizeHandleCollection(8, 8);
        ResizeHandles.SetLocation(Bounds);
    }

    //...(省略)

    public virtual void Draw(IGraphics g)
    {
        if (Fill is not null)
        {
            g.FillRectangle(Bounds, Fill);
        }
        if (Stroke is not null)
        {
            g.DrawRectangle(Bounds, Stroke);
        }
        //リサイズハンドルをまとめて描画
        ResizeHandles.Draw(g);
    }

    public virtual HitResult HitTest(Point p)
    {
        //リサイズハンドルにヒットしたらそのハンドルのHitResultを返却
        //図形本体にヒットしたらHitResult.Bodyを返却
        //ヒットしなかった場合はHitResult.Noneを返却
        var hitResult = ResizeHandles.HitTest(p);
        if (hitResult is not HitResult.None)
        {
            return hitResult;
        }
        return HitTestStrategy.HitTest(p, this) ? HitResult.Body : HitResult.None;
    }

    public virtual void Drag(Point oldPointer, Point currentPointer)
    {
        //ResizeHandleがActive(=ドラッグ対象)の場合
        //ハンドルのResize()メソッドを実行して、結果をRoundsに設定する
        if (ResizeHandles.ActiveHandle is not null)
        {
            SetBounds(ResizeHandles.Resize(currentPointer, Bounds));
            return;
        }
        //ハンドル以外では、図形全体の移動処理を行う
        var (dx, dy) = (currentPointer.X - oldPointer.X, currentPointer.Y - oldPointer.Y);
        SetBounds(new Rectangle(Bounds.Left + dx, Bounds.Top + dy, Bounds.Size.Width, Bounds.Size.Height));
    }

    protected void SetBounds(Rectangle bounds)
    {
        //図形の座標(Bounds)変更時、それに合わせてリサイズハンドルの位置も更新する
        Bounds = bounds;
        ResizeHandles.SetLocation(bounds);
    }
}

ここまででライブラリ(CoreShape)側準備はできました。

マウスポインタがヒットした部位によってカーソルの形状を変更しよう

WPFアプリ側では大きな変更はありませんが、ヒットした部位に応じてマウスカーソルの形状を変更する必要があります。

MouseMoveイベントハンドラ内でHitTest()で返却されたHitResultの値を見て、それに合ったカーソルに変更する処理を追加します。

private void sKElement_MouseMove(object sender, MouseEventArgs e)
{
    var p = e.GetPosition(skElement);
    var currentPoint = new CoreShape.Point((float)p.X, (float)p.Y);

    if (e.LeftButton == MouseButtonState.Pressed)
    {
        if (activeShape is null)
        { return; }
        activeShape.Drag(oldPoint, currentPoint);
        skElement.InvalidateVisual();
    }
    else
    {
        Cursor = Cursors.Arrow;
        activeShape = null;
        foreach (var shape in shapes)
        {
            var hitResult = shape.HitTest(currentPoint);
            //ヒットした部位に応じてカーソルの形状を変更する
            switch (hitResult)
            {
                case HitResult.Body:
                    Cursor = Cursors.SizeAll;
                    break;
                case HitResult.ResizeN:
                case HitResult.ResizeS:
                    Cursor = Cursors.SizeNS;
                    break;
                case HitResult.ResizeE:
                case HitResult.ResizeW:
                    Cursor = Cursors.SizeWE;
                    break;
                case HitResult.ResizeNW:
                case HitResult.ResizeSE:
                    Cursor = Cursors.SizeNWSE;
                    break;
                case HitResult.ResizeNE:
                case HitResult.ResizeSW:
                    Cursor = Cursors.SizeNESW;
                    break;
            }
            if (hitResult is not HitResult.None)
            {
                activeShape = shape;
                break;
            }
        }
    }
    oldPoint = currentPoint;
}

動作確認

これでひとまず動かせる状態にはなりました。では、実際に動かしてみましょう!

coreShape_capter3-1.gif

うん。ちゃんと動いていますね。

次回

ここまでで、基本的な動作の実装は完了しました。ですが、これではまだ下記のような問題があります。

  1. 図形が選択状態の時のみリサイズハンドルを表示したいが、常に表示されている
  2. ドラッグしているハンドルの反対側の境界を越えてドラッグするとBoundsの幅高さがマイナスになる
    その状態になると当たり判定が正常に動作しない

次回はこのあたりの問題を解消するよう実装を進めていきたいと思います。

  • Chapter3 ドラッグ操作で図形のサイズを変更してみよう(その2)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む