WPFのInkCanvasで塗りつぶす
InkCanvasは線を描くものっぽくて閉じた領域の塗りつぶし、みたいなことは標準ではできなさそうでした
ということでとりあえず適当に塗りつぶすようにしていみます
塗りつぶす人
System.Windows.Ink.Stroke
を継承してDrawCore
で塗りつぶすようにします
ソース
public class CustomInkCanvas : InkCanvas { protected override void OnStrokeCollected(InkCanvasStrokeCollectedEventArgs e) { this.Strokes.Remove(e.Stroke); var newStroke = new FillStroke(e.Stroke.StylusPoints, e.Stroke.DrawingAttributes); this.Strokes.Add(newStroke); base.OnStrokeCollected(new InkCanvasStrokeCollectedEventArgs(newStroke)); } } public class FillStroke : Stroke { public FillStroke(StylusPointCollection stylusPoints) : base(stylusPoints) { } public FillStroke(StylusPointCollection stylusPoints, DrawingAttributes drawingAttributes) : base(stylusPoints, drawingAttributes) { } protected override void DrawCore(DrawingContext drawingContext, DrawingAttributes drawingAttributes) { if (drawingContext == null) { throw new ArgumentNullException(nameof(drawingContext)); } if (null == drawingAttributes) { throw new ArgumentNullException(nameof(drawingAttributes)); } if (this.StylusPoints.Count < 3) return; var stroke = new Pen(Brushes.Blue, 2); stroke.Freeze(); var fill = new SolidColorBrush(Color.FromArgb(0x80, 0x00, 0xff, 0xff)); fill.Freeze(); var streamGeometry = new StreamGeometry(); using (var geometryContext = streamGeometry.Open()) { geometryContext.BeginFigure(this.StylusPoints[0].ToPoint(), true, false); var points = new PointCollection(); for (var i = 1; i < this.StylusPoints.Count; i++) { points.Add(this.StylusPoints[i].ToPoint()); } geometryContext.PolyLineTo(points, true, false); } drawingContext.DrawGeometry(fill, stroke, streamGeometry); } }
CustomInkCanvas
はStrokeが追加されたときにFillStroke
に置き換える処理をしています
FillStroke.DrawCore
で塗りつぶしています
結果
塗りつぶせました
あとは閉じた領域だけ塗るようにすればいい感じになりそうなんですが、閉じた領域ってどうやって探すんだろう・・・
WPFのItemsControl.ItemTemplateでリストの一つ前を参照する
WPFのItemsControlはすごく強力だとおもいますよね
ItemsControl.ItemsSourceにリストをバインドして、ItemTemplateで中身を定義すると自由なリストが簡単に作れるので一度使うと癖になります
簡単な例だとこんな感じ
<ItemsControl ItemsSource="{Binding Numbers}"> <ItemsControl.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal" Margin="5"> <TextBlock Text="{Binding}"/> </StackPanel> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl>
Numbersはランダムな10個の数値リストです
実行結果はランダムな数値が縦に10個並ぶだけなので割愛
普段はこんな感じでお手軽に使えますがごくごくごく偶にアイテムの前の値が欲しい時があります(主にデータ設計に問題がある時ですかねぇ)
そんな時は
{Binding RelativeSource={RelativeSource PreviousData}}
で見ることができるんですね、今日知りました
先ほどのサンプルを更新すると
<ItemsControl Grid.Column="1" ItemsSource="{Binding Numbers}"> <ItemsControl.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal" Margin="5"> <TextBlock Text="{Binding RelativeSource={RelativeSource PreviousData}}"/> <TextBlock Text=" -> "/> <TextBlock Text="{Binding}"/> </StackPanel> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl>
前の値(リストの一つ上の項目)から今回の値に変わったよ、的な表示をしています
実行結果は
左側は初めに書いたもにで、右が更新後です
こんなことも簡単にできるんですねー
WPFつよい
WPFでShapeをResourceにして使いまわす
WPFに限らず今時のアプリではアプリ内で使うアイコン等をベクトルデータで持っていることが多いと思います
Illustrator → SVG → XAML
って感じでアイコンデータがやってきたりすると思いますが、これをそのままContentに張り付けるのは悪手ですよね(面倒な時を除く)
ということでアイコンデータはResourceDictionaryに入れて、使うところはKeyを指定するようにしましょう
Resource(App.xaml)
<Application.Resources> <Canvas x:Key="Icon1" x:Shared="False" Width="100" Height="100"> <Ellipse Width="100" Height="100" Fill="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type ButtonBase}}}"/> </Canvas> <Canvas x:Key="Icon2" x:Shared="False" Width="100" Height="100"> <Rectangle Width="100" Height="100" Fill="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type ButtonBase}}}"/> </Canvas> </Application.Resources>
MainWindow.xaml
<Grid> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <Button Grid.Row="0" Grid.Column="0" Content="{StaticResource Icon1}"/> <Button Grid.Row="0" Grid.Column="1" Content="{StaticResource Icon1}" Foreground="Blue"/> <Button Grid.Row="1" Grid.Column="0" Content="{StaticResource Icon2}"/> <Button Grid.Row="1" Grid.Column="1" Content="{StaticResource Icon2}" Foreground="Blue"/> </Grid>
結果
ポイント
Shape.Fill
Shape.Fillの指定ですが、ContentControlのForegroundを参照させたい時(Button.Foregroundとか)は上のような感じにしないと参照してくれません
TextBlockとかは何も指定しなければ親のForegroundを見てくれるのでそのつもりでいると???ってなります
上の場合ButtonBaseを継承しているコントロール内でしか効果ありませんが、まぁそれ以外の状況で色を適宜変えたいとかないよね、ね
ということでMainWindow.xamlのGrid.Column="1"にあるボタンではForegroundの指定色にShapeが変更されていることがわかります
x:Shared="False"
ResourceDictionaryに定義しているリソースはデフォルトで使いまわすようになっています(Brushとかそういうのは使いまわした方が効率いいですからね)
ですがCanvasは使いまわしちゃいけないですよね
x:Sharedを書かないとどこか一つにしか出てこなくなります
Buttonの中のContentControlのContentが何かのタイミングで消える
— ぷにお (@vl_o_lv) November 14, 2016
x:Shared="False"とすればそれぞれにインスタンスを作ってくれます
これでかつる!WPFつおい
2016/11/18 追記
Fill="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type ButtonBase}}}"
より
Fill="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type ContentControl}}}"
の方がよさそうですね (ButtonBaseをContentControlに変更)