- 投稿日:2021-01-15T23:04:12+09:00
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 }, };実行
- 投稿日:2021-01-15T23:04:12+09:00
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 }, };実行
- 投稿日:2021-01-15T18:25:52+09:00
.NET5.0/C#9.0でオートシェイプ風図形描画ライブラリを作ろう!(Chapter3-1)
※本記事は下記のエントリから始まる連載記事となります。
.NET5.0/C#9.0でオートシェイプ風図形描画ライブラリを作ろう!(Chapter0)Chapter3 ドラッグ操作で図形のサイズを変更してみよう(その1)
今回は下準備として用意するものが多いので、複数回に分割して書きます。
ソースコード
Capter3の内容は下記ブランチにて実装されています。実装の詳細はこちらをご確認ください。
https://github.com/pierre3/CoreShape/tree/blog/capter3リサイズハンドルを定義しよう
通常図形のサイズを変更する場合、下図の
□
マークのような"つまみ"を操作すると思います。
まずは、この"つまみ"を表現するクラスを用意するところから始めましょう。
なお、以降ではこの"つまみ"の事を「リサイズハンドル」と呼ぶことにします。リサイズハンドルの種別
リサイズハンドルは、その位置によって動作が異なります。また、マウスポインタを重ねた際のカーソルの形状も異ります。
そこで、当たり判定時でヒットした際にマウスポインタが図形のどの部分に位置しているかを示す
HitResult
列挙型を定義します。public enum HitResult { None, ResizeN, ResizeNE, ResizeE, ResizeSE, ResizeS, ResizeSW, ResizeW, ResizeNW, Body }ResizeXXが各リサイズハンドルの位置に対応しています。
Resizeの後ろのアルファベットは、画面上を北とした際の方角の頭文字を表しています。
(例えば N = North, NE = North East)
また、
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; }動作確認
これでひとまず動かせる状態にはなりました。では、実際に動かしてみましょう!
うん。ちゃんと動いていますね。
次回
ここまでで、基本的な動作の実装は完了しました。ですが、これではまだ下記のような問題があります。
- 図形が選択状態の時のみリサイズハンドルを表示したいが、常に表示されている
- ドラッグしているハンドルの反対側の境界を越えてドラッグするとBoundsの幅高さがマイナスになる
その状態になると当たり判定が正常に動作しない次回はこのあたりの問題を解消するよう実装を進めていきたいと思います。
- Chapter3 ドラッグ操作で図形のサイズを変更してみよう(その2)