dhq_boiler
@dhq_boiler

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

[WPF]背景の境界線の太さの変更が反映されない

お世話になっております。

解決したいこと

C# + WPFでベクターグラフィックスドローイングツール"boiler's Graphics"を開発しています。ライブラリはPrism 8.1.97やReactiveProperty 7.11.0、OpenCvSharp4 4.5.2.20210404等を使用させてもらっています。

2021-08-22.png

発生している問題・エラー

レイヤー機能を実装完了して、バージョン3.0のリリースを前にバグを取り除く作業をしています。それで、取り除こうとしているバグが上手く取り除けないのでQiitaに質問を書かせてもらっています。

当アプリケーションには編集したベクターグラフィックスをJpegやPngファイルに書き出す機能があります。これをエクスポート機能と呼んでいます。

このバグはエクスポートする時に、背景オブジェクトの境界線の太さを0にして画像に出力する処理をするようにコードを書いたのですが、境界線の太さが初期値(アプリケーション起動時に10を設定しましたが、10のまま)の状態で出力されてしまい、エクスポートされた画像ファイルに背景オブジェクトの境界線が出力されてしまいます。
背景オブジェクトの境界線はエクスポートする瞬間は消えてほしいのですが、編集時は見えるようにしておきたいです。
どうしたら望む結果が得られるでしょうか?

ちなみに、エクスポートする瞬間の、ファイル出力結果には境界線(太さ=10)が含まれてしまっていますが、エクスポート完了後は境界線は太さ=1になります。ReactivePropertyによるデータコンテキストの変更通知は上手くいっているようですが、Viewへの反映が上手くいっていないようです。

ViewModels/DiagramViewModel.cs
    public class DiagramViewModel : BindableBase, IDiagramViewModel, IDisposable
    {
        public DiagramViewModel(int width, int height)
        {
            :
            InitialSetting();
        }

        private void InitialSetting()
        {
            EdgeColors.Add(Colors.Black);
            FillColors.Add(Colors.White);
            EdgeThickness.Value = 1.0;
            CanvasBorderThickness = 0.0;
            CanvasBackground.Value = Colors.White;

            //背景オブジェクトの登録
            BackgroundItem.Value = new BackgroundViewModel();
            BackgroundItem.Value.ZIndex.Value = -1;
            BackgroundItem.Value.FillColor.Value = CanvasBackground.Value;
            BackgroundItem.Value.Left.Value = 0;
            BackgroundItem.Value.Top.Value = 0;
            BackgroundItem.Value.Width.Value = Width;
            BackgroundItem.Value.Height.Value = Height;
            BackgroundItem.Value.Owner = this;
            BackgroundItem.Value.EdgeColor.Value = Colors.Black;
            BackgroundItem.Value.EdgeThickness.Value = 10; //バグ視認しやすさ重視で太くする
            BackgroundItem.Value.EnableForSelection.Value = false;
            BackgroundItem.Value.IsVisible.Value = true;

            EnablePointSnap.Value = true;
            Layer.LayerCount = 1;
            LayerItem.LayerItemCount = 1;
            var layer = new Layer();
            layer.IsVisible.Value = true;
            layer.IsSelected.Value = true;
            layer.Name.Value = Name.GetNewLayerName();
            Random rand = new Random();
            layer.Color.Value = Randomizer.RandomColor(rand);
            Layers.Add(layer);
        }
        :
    }
ViewModels/ExportViewModel.cs
class ExportViewModel : BindableBase, IDialogAware, IDisposable
    {
        :
        public ReactiveProperty<string> Path { get; set; } = new ReactiveProperty<string>();

        public ReactiveCommand ExportCommand { get; set; }

        :
        public ExportViewModel()
        {
            :
            ExportCommand = Path
                           .Where(x => x != null)
                           .Select(x => x.Length > 0)
                           .ToReactiveCommand();
            ExportCommand.Subscribe(_ => ExportProcess())
                         .AddTo(_disposables);
            :
            Path.Value = "";
        }

        private void ExportProcess()
        {
            Dictionary<SelectableDesignerItemViewModelBase, bool> tempIsSelected;

            BeforeExport(out tempIsSelected);

            Export();

            AfterExport(tempIsSelected);

            OkClose();
        }

        private void BeforeExport(out Dictionary<SelectableDesignerItemViewModelBase, bool> tempIsSelected)
        {
            var diagramControl = App.Current.MainWindow.GetChildOfType<DiagramControl>();
            var itemsControl = diagramControl.GetChildOfType<ItemsControl>();
            tempIsSelected = new Dictionary<SelectableDesignerItemViewModelBase, bool>();
            var diagramViewModel = diagramControl.DataContext as DiagramViewModel;
            var backgroundItem = diagramViewModel.BackgroundItem.Value;

            foreach (var item in itemsControl.Items.Cast<SelectableDesignerItemViewModelBase>())
            {
                tempIsSelected.Add(item, item.IsSelected.Value);
                //一時的に選択状態を解除する
                item.IsSelected.Value = false;
            }

            foreach (var snapPointVM in itemsControl.Items.OfType<SnapPointViewModel>())
            {
                //一時的に非表示にする
                snapPointVM.Opacity.Value = 0;
            }

            //一時的に背景の境界線を消す
            backgroundItem.EdgeThickness.Value = 0; //not working
        }

        private void Export()
        {
            var diagramControl = App.Current.MainWindow.GetChildOfType<DiagramControl>();
            var itemsControl = diagramControl.GetChildOfType<ItemsControl>();
            var designerCanvas = diagramControl.GetChildOfType<DesignerCanvas>();
            var diagramViewModel = diagramControl.DataContext as DiagramViewModel;
            var backgroundItem = diagramViewModel.BackgroundItem.Value;

            var rtb = new RenderTargetBitmap((int)diagramViewModel.Width, (int)diagramViewModel.Height, 96, 96, PixelFormats.Pbgra32);

            DrawingVisual visual = new DrawingVisual();
            using (DrawingContext context = visual.RenderOpen())
            {
                //背景を描画
                RenderDesignerItemViewModelBase(context, backgroundItem); //※1 backgroundItem.EdgeThicknessはこの時点では0になっている、加えて当メソッド内のviewのStrokeThicknessも0になっている

                foreach (var item in diagramViewModel.AllItems.Value.Except(new SelectableDesignerItemViewModelBase[] { backgroundItem }).OrderBy(x => x.ZIndex.Value))
                {
                    if (item is DesignerItemViewModelBase designerItem)
                    {
                        RenderDesignerItemViewModelBase(context, designerItem);
                    }
                    else if (item is ConnectorBaseViewModel connector)
                    {
                        RenderConnectorBaseViewModel(context, connector);
                    }
                }
            }

            rtb.Render(visual);

            OpenCvSharpHelper.ImShow("test", rtb); //デバッグ用RenderTargetBitmap->OpenCvSharp.Matに変換してCv2.ImShowする

            var generator = FileGenerator.Create(System.IO.Path.GetExtension(Path.Value));
            generator.AddFrame(BitmapFrame.Create(rtb));
            generator.SetQualityLevel(QualityLevel.Value);
            generator.Save(Path.Value);
        }

        private void AfterExport(Dictionary<SelectableDesignerItemViewModelBase, bool> tempIsSelected)
        {
            var diagramControl = App.Current.MainWindow.GetChildOfType<DiagramControl>();
            var itemsControl = diagramControl.GetChildOfType<ItemsControl>();
            var diagramViewModel = diagramControl.DataContext as DiagramViewModel;
            var backgroundItem = diagramViewModel.BackgroundItem.Value;

            //背景の境界線を復元する
            backgroundItem.EdgeThickness.Value = 1;

            //IsSelectedの復元
            foreach (var item in itemsControl.Items.Cast<SelectableDesignerItemViewModelBase>())
            {
                bool outIsSelected;
                if (tempIsSelected.TryGetValue(item, out outIsSelected))
                {
                    item.IsSelected.Value = outIsSelected;
                }
            }

            foreach (var snapPointVM in itemsControl.Items.OfType<SnapPointViewModel>())
            {
                //スナップポイントを半透明に復元する
                snapPointVM.Opacity.Value = 0.5;
            }
        }

        private static void RenderConnectorBaseViewModel(DrawingContext context, ConnectorBaseViewModel connector)
        {
            var diagramControl = App.Current.MainWindow.GetChildOfType<DiagramControl>();
            var view = diagramControl.GetCorrespondingViews<FrameworkElement>(connector).First(x => x.GetType() == connector.GetViewType());
            VisualBrush brush = new VisualBrush(view);
            context.DrawRectangle(brush, null, new Rect(connector.LeftTop.Value, new Size(connector.Width.Value, connector.Height.Value)));
        }

        private static void RenderDesignerItemViewModelBase(DrawingContext context, DesignerItemViewModelBase designerItem)
        {
            var diagramControl = App.Current.MainWindow.GetChildOfType<DiagramControl>();
            var view = diagramControl.GetCorrespondingViews<FrameworkElement>(designerItem).First(x => x.GetType() == designerItem.GetViewType());
            VisualBrush brush = new VisualBrush(view);
            context.DrawRectangle(brush, null, new Rect(new Point(designerItem.Left.Value, designerItem.Top.Value), new Size(designerItem.Width.Value, designerItem.Height.Value)));
        }
        :
    }

背景オブジェクトはBackgroundViewModelクラスのインスタンスで、BackgroundDataTemplate.xamlに定義されているDataTemplateが適用されます。

Resources/DesignerItems/BackgroundDataTemplate.xaml
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:convereter="clr-namespace:boilersGraphics.Converters"
    xmlns:local="clr-namespace:boilersGraphics.Resources.DesignerItems"
    xmlns:viewModel="clr-namespace:boilersGraphics.ViewModels">
    <convereter:ToSolidColorBrushConverter x:Key="solidColorBrushConverter" />
    <DataTemplate DataType="{x:Type viewModel:BackgroundViewModel}">
        <Path Stroke="{Binding EdgeColor.Value, Converter={StaticResource solidColorBrushConverter}}"
              StrokeThickness="{Binding EdgeThickness.Value}"
              IsHitTestVisible="False"
              Stretch="Fill"
              Fill="{Binding FillColor.Value, Converter={StaticResource solidColorBrushConverter}}"
              Data="{Binding PathGeometry.Value}" />
    </DataTemplate>
</ResourceDictionary>

掲載したソースコードの場所一覧

  • ViewModels/DiagramViewModel.cs
  • ViewModels/ExportViewModel.cs
  • Resources/DesignerItems/BackgroundDataTemplate.xaml

ソースコード

boiler's Graphics
https://github.com/dhq-boiler/boiler-s-Graphics

gitリポジトリ
https://github.com/dhq-boiler/boiler-s-Graphics.git

ブランチ:develop

コミット:bb59418

何か私の見落とし、致命的な勘違いなど気づいたところがあれば、回答していただけると助かります。当プロジェクトのソースコードは意外にも巨大なので、検証に苦労するかもしれませんが、よろしくお願いいたします。

当アプリの使い方

当アプリケーションboiler's Graphicsの使い方が知りたいという方のために、使い方紹介ページを用意しました。是非参考にしてください。→HowToUse

0

2Answer

自己解決しました。

試したこと

6, 7は正しく動作しませんでした。

8でUpdateLayout()を実行したら、正しく動作(背景の境界線の太さが0になる)しました。

        private static void RenderDesignerItemViewModelBase(DrawingContext context, DesignerItemViewModelBase designerItem)
        {
            var diagramControl = App.Current.MainWindow.GetChildOfType<DiagramControl>();
            var view = diagramControl.GetCorrespondingViews<FrameworkElement>(designerItem).First(x => x.GetType() == designerItem.GetViewType());
            VisualBrush brush = new VisualBrush(view);

            :

            //#6 not working
            //view.CoerceValue(System.Windows.Shapes.Shape.StrokeThicknessProperty);

            //#7 not working
            //view.SetCurrentValue(System.Windows.Shapes.Shape.StrokeThicknessProperty, designerItem.EdgeThickness.Value);

            //#8 worked correctly
            view.UpdateLayout();

            context.DrawRectangle(brush, null, new Rect(new Point(designerItem.Left.Value, designerItem.Top.Value), new Size(designerItem.Width.Value, designerItem.Height.Value)));
        }
1Like

試したこと

Invalidate~メソッドを試してみましたが、ダメでした。背景オブジェクトの境界線は太さ10のままでした。

        private static void RenderDesignerItemViewModelBase(DrawingContext context, DesignerItemViewModelBase designerItem)
        {
            var diagramControl = App.Current.MainWindow.GetChildOfType<DiagramControl>();
            var view = diagramControl.GetCorrespondingViews<FrameworkElement>(designerItem).First(x => x.GetType() == designerItem.GetViewType());
            VisualBrush brush = new VisualBrush(view);
            
            //#1 not working
            //brush.InvalidateProperty(System.Windows.Shapes.Shape.StrokeThicknessProperty);
            
            //#2 not working
            //view.InvalidateVisual();
            
            //#3 not working
            //view.InvalidateProperty(System.Windows.Shapes.Shape.StrokeThicknessProperty);
            
            //#4 not working
            //view.InvalidateArrange();

            //#5 not working
            //view.InvalidateMeasure();
            //view.InvalidateArrange();

            context.DrawRectangle(brush, null, new Rect(new Point(designerItem.Left.Value, designerItem.Top.Value), new Size(designerItem.Width.Value, designerItem.Height.Value)));
        }

0Like

Your answer might help someone💌