午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

WPF、Rectangleとかに2色の破線(点線)枠表示

結果

結果
サイズ100x50のRectangleに破線(点線)枠表示
破線パターンの色は赤と白の2色、これを1ピクセルごと交互に配置
これを5種類の方法で表示したところ

方法
1~4は全く同じ結果、5は微妙に違う

コード

github.com

XAML(デザイン画面)

<Window x:Class="_20220528_2ColorDashStroke.MainWindow"
        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:local="clr-namespace:_20220528_2ColorDashStroke"
        mc:Ignorable="d"
        Title="MainWindow" Height="250" Width="300">
  <Canvas x:Name="MyCanvas" Background="Black"
          UseLayoutRounding="True" RenderOptions.EdgeMode="Aliased">

    <Grid Canvas.Left="10" Canvas.Top="10">
      <Rectangle Stroke="Red" Width="100" Height="50"/>
      <Rectangle Stroke="White" StrokeDashArray="1.0"/>
    </Grid>

    <Rectangle Width="100" Height="50" Canvas.Left="10" Canvas.Top="70">
      <Rectangle.Stroke>
        <VisualBrush Stretch="None">
          <VisualBrush.Visual>
            <Grid RenderOptions.EdgeMode="Aliased">
              <Rectangle Stroke="Red" Width="100" Height="50"/>
              <Rectangle Stroke="White" StrokeDashArray="1.0"/>
            </Grid>
          </VisualBrush.Visual>
        </VisualBrush>
      </Rectangle.Stroke>
    </Rectangle>
    
  </Canvas>
</Window>



MainWindow.xaml.cs

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;

//2色の破線枠のテスト
namespace _20220528_2ColorDashStroke
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            Test2VisualBrush();
            Test1RectangleRectangle();
            Test3ImageBrush();
        }

        //Rectangleを2つをそのまま使う
        private void Test1RectangleRectangle()
        {
            Rectangle r1 = new() { Width = 100, Height = 50, Stroke = Brushes.Red };
            Rectangle r2 = new()
            {
                Width = 100,
                Height = 50,
                Stroke = Brushes.White,
                StrokeDashArray = new DoubleCollection() { 1.0 }
            };

            Grid g = new(); RenderOptions.SetEdgeMode(g, EdgeMode.Aliased);
            Canvas.SetLeft(g, 120); Canvas.SetTop(g, 10);
            g.Children.Add(r1); g.Children.Add(r2);
            MyCanvas.Children.Add(g);
        }

        //Rectangleを2つをVisualBrushとして使う
        private void Test2VisualBrush()
        {
            double w = 100; double h = 50;
            Rectangle br1 = new() { Width = w, Height = h, Stroke = Brushes.Red };
            Rectangle br2 = new()
            {
                Width = w,
                Height = h,
                Stroke = Brushes.White,
                StrokeDashArray = new DoubleCollection() { 1.0 }
            };
            Grid g = new(); RenderOptions.SetEdgeMode(g, EdgeMode.Aliased);
            g.Children.Add(br1); g.Children.Add(br2);
            VisualBrush vb = new(g);

            Rectangle r = new() { Width = w, Height = h };
            Canvas.SetLeft(r, 120); Canvas.SetTop(r, 70);
            r.Stroke = vb;
            MyCanvas.Children.Add(r);
        }

        //bitmapで破線パターンを作成してImageBrushとして使う
        private void Test3ImageBrush()
        {
            ImageBrush? b = My2ColorDashBrush();
            Rectangle r = new() { Width = 100, Height = 50, Stroke = b, StrokeThickness = 1.0 };
            Canvas.SetLeft(r, 120); Canvas.SetTop(r, 130);
            MyCanvas.Children.Add(r);
        }
        private ImageBrush My2ColorDashBrush()
        {
            WriteableBitmap bitmap = MakeCheckPattern(1, Colors.Red, Colors.White);
            ImageBrush brush = new(bitmap)
            {
                Stretch = Stretch.None,
                TileMode = TileMode.Tile,
                Viewport = new Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight),
                ViewportUnits = BrushMappingMode.Absolute
            };
            return brush;
        }
        //       無限の透明市松模様をWriteableBitmapとImageBrushのタイル表示で作成 - 午後わてんのブログ
        //https://gogowaten.hatenablog.com/entry/15917385
        /// <summary>
        /// 指定した2色から市松模様のbitmapを作成
        /// </summary>
        /// <param name="cellSize">1以上を指定、1指定なら2x2ピクセル、2なら4x4ピクセルの画像作成</param>
        /// <param name="c1"></param>
        /// <param name="c2"></param>
        /// <returns></returns>
        private WriteableBitmap MakeCheckPattern(int cellSize, Color c1, Color c2)
        {
            int width = cellSize * 2;
            int height = cellSize * 2;
            var wb = new WriteableBitmap(width, height, 96, 96, PixelFormats.Bgra32, null);
            int stride = wb.Format.BitsPerPixel / 8 * width;
            byte[] pixels = new byte[stride * height];
            Color iro;
            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    if ((y < cellSize & x < cellSize) | (y >= cellSize & x >= cellSize))
                    {
                        iro = c1;
                    }
                    else { iro = c2; }

                    int p = y * stride + x * 4;
                    pixels[p] = iro.B;
                    pixels[p + 1] = iro.G;
                    pixels[p + 2] = iro.R;
                    pixels[p + 3] = iro.A;
                }
            }
            wb.WritePixels(new Int32Rect(0, 0, width, height), pixels, stride, 0);
            return wb;
        }

        //未使用
        //白と指定色を横に交互に並べた画像、縦1ピクセル
        private WriteableBitmap MakePattern2(int length, Color color)
        {
            int width = length * 2;
            WriteableBitmap wb = new(width, 1, 96, 96, PixelFormats.Rgb24, null);
            int stride = wb.Format.BitsPerPixel / 8 * width;
            byte[] pixels = new byte[stride];
            for (int x = 0; x < length; x += 3)
            {
                pixels[x] = color.R;
                pixels[x + 1] = color.G;
                pixels[x + 2] = color.B;
            }
            for (int x = length; x < width; x += 3)
            {
                pixels[x] = 255;
                pixels[x + 1] = 255;
                pixels[x + 2] = 255;
            }
            wb.WritePixels(new Int32Rect(0, 0, width, 1), pixels, stride, 0);
            return wb;
        }

    }
}

0.ぼやけるの禁止

XAML(デザイン画面)
10行目
重要
UseLayoutRounding="True"
RenderOptions.EdgeMode="Aliased"
今回も表示はくっきりさせたいので、この2つを指定、重要

1.XAML+Rectangleそのまま

Rectangleそのまま
Gridパネルの中に同じサイズのRectangleを2つ重ねているだけ
下のRectangleは赤の実線枠を表示して、上のRectangleは白の破線枠を表示
この方法のいいところは4行で済むので楽
気になるところは、要素を3つも表示するから、他の方法よりもリソース消費が大きいかも?
そうだとしても1000個とか表示しなければ違いはでなさそうなので、この方法が一番いいかも

2.XAML+VisualBrush

XAML+VisualBrush
1番の方法で表示したものからVisualBrushを作成して、それを枠(Stroke)のブラシに指定する感じ
21行目、
Grid RenderOptions.EdgeMode="Aliased"
これが必須で、指定なしだと
指定なし
ぼやけて赤と白が混じって表示されて、破線じゃなくなる
一方、19行目の
VisualBrush Stretch="None" これは指定しなくても問題なかったけど、まあいいや


3.1番をC#

これは1番の方法をC#で書いただけ

3番の方法
長い
XAMLだと4行で済んでいたのに長い
C#で書くほうが好きだけど、これだけ違うと事情がない限りXAMLで書く

4.2番をC#

これも2番のXAMLC#で書いただけ

4番の方法
XAMLでも12行あったから、それほど増えていない感じがする

5.ImageBrush

破線を表現する画像を作成して、それを元にImageBrushを作成、それを枠線(Stroke)に指定

5-1.破線画像作成

破線画像作成
これは
無限の透明市松模様をWriteableBitmapとImageBrushのタイル表示で作成
このときのを応用
これに赤と白、サイズ1ピクセルを指定してできあがる画像は
破線画像確認
マウスカーソルの先にあるのがそれ
拡大してみると
64倍拡大表示した破線画像
指定した2色が交互に配置されている

5-2.ImageBrushを作成

破線画像からImageBrushを作成

ImageBrush作成
色々指定しているのはどれもぼやけるのをなくすためけど、必須だったのは74と76行目だけだったかな、それでも引き伸ばされたりアンチエイリアスがかからないように全部指定した

5-.3.StrokeにImageBrushを指定

あとは

ImageBrushを指定
これで完了
5番の方法は1~4に比べてかなりめんどくさいけど

違い
1~4までの方法だと角のパターンで同じ色が連続して表示されることがある、5の方法なら期待通り



問題

線を太くしたとき

1番での調整は

先の太さ変更、1から10

5番の調整は
64行目の
Rectangle r = new() { Width = 100, Height = 50, Stroke = b, StrokeThickness = 1.0 };
から
Rectangle r = new() { Width = 100, Height = 50, Stroke = b, StrokeThickness = 10.0 };

70行目の
WriteableBitmap bitmap = MakeCheckPattern(1, Colors.Red, Colors.White);
から
WriteableBitmap bitmap = MakeCheckPattern(10, Colors.Red, Colors.White);に変更

結果

StrokeThickness="10"
1~4の方法だと角のところがいまいちな表示になる、一方5番は調整すれば期待どおりになる

枠表示する要素のサイズ変更

今までは100x50と切の良い数値のサイズだったのを
101x51にしてみると

半端なサイズに枠表示
1番はさっきと大差ないけど、5番が残念な表示になっている
枠表示する要素のサイズが太さの整数倍以外だとこうなってしまうはずで、これは調整じゃ解決できないかなあ

枠の太さ1ピクセルなら違和感ない

太さ4、2x2パターン

破線が1色じゃあかんの?

黒背景に白破線
黒背景に白破線、全く問題ない
これが青破線だと
黒背景に青破線
見づらい、さらに黒破線なら全く見えなくなる
背景色が決まっているとかなら問題なけど、そうじゃない場合は2色使ったほうが安定する
2色破線

白背景になっても

識別できる



関連記事

次回のWPF記事
gogowaten.hatenablog.com

前回のWPF記事は171日前
gogowaten.hatenablog.com

5年前
gogowaten.hatenablog.com

5年前
gogowaten.hatenablog.com

gogowaten.hatenablog.com

2年後
WPFでテストアプリ、複数選択と枠表示 - 午後わてんのブログ