0

The following code is an adaptation of the code example on page 502 of Adam Nathan, WPF 4.5 unleashed.

Following the advice there I created a class VisualHostClass that "hosts" a list of DrawingVisuals, thereby making them displayable. This worked so far.

Now I wanted to react to a left mouse click by changing the color of the displayed visual. The code for this is in the method HitTestCallback.

With the included Trace.WriteLine statements I can see that the relevant line

drw.Brush = Brushes.Red;

is reached and the property Brush has a new value afterwards. But: The display does not change, the clicked-on DrawingVisual is still Yellow afterwards (that was the brush color used on creation).

What can/should I do here? (I am absolute beginner with WPF).

Thanks in advance for all answers!

    public partial class VisualHostClass : FrameworkElement
    {
        public VisualHostClass()
        {
 
        }

        public List<DrawingVisual> myVisuals = new List<DrawingVisual>();

        public void AddVisual(DrawingVisual dvs)
        {
            myVisuals.Add(dvs);
            AddVisualChild(dvs);
            AddLogicalChild(dvs);

        }

        protected override int VisualChildrenCount
        {
            get { return myVisuals.Count; }
        }

        protected override Visual GetVisualChild(int index)
        {
            if (index < 0 || index >= myVisuals.Count)
            {
                throw new ArgumentOutOfRangeException("index");
            }

            return myVisuals[index];
        }

        public void removeAllChilds()
        {
            foreach (DrawingVisual dvs in myVisuals)
            {
                RemoveVisualChild(dvs);
                RemoveLogicalChild(dvs);
            }
            myVisuals.Clear();
        }

        protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            base.OnMouseLeftButtonDown(e);

            Point location = e.GetPosition(this);

            VisualTreeHelper.HitTest(this, null,
                new HitTestResultCallback(HitTestCallback),
                new PointHitTestParameters(location));

        }

        public HitTestResultBehavior HitTestCallback(HitTestResult result)
        {
            if (result.VisualHit.GetType() == typeof(DrawingVisual))
            {
                DrawingVisual dv = result.VisualHit as DrawingVisual;

                Trace.WriteLine("HitTestCallback: dv = " + dv.ToString());

                foreach(GeometryDrawing drw in dv.Drawing.Children)
                {
                    Trace.WriteLine("HitTestCallback: drw = " + drw.ToString());
                    
                    Trace.WriteLine("HitTestCallback: drw.Geometry = " + drw.Geometry.ToString());

                    Trace.WriteLine("HitTestCallback: drw.Brush = " + drw.Brush.ToString());

                    drw.Brush = Brushes.Red;

                    Trace.WriteLine("HitTestCallback: drw.Brush = " + drw.Brush.ToString());

                    Trace.WriteLine("HitTestCallback: drw.IsFrozen = " + drw.IsFrozen.ToString());

                }
 
            }

            return HitTestResultBehavior.Continue;
        }
    }
}

EDIT: After some experimenting the following worked (but it is not satifying for me, as I can only change the Color of a SolidColorBrush, but not the Brush itself):

   public void ChangeBrush(DrawingVisual dv)
    {
        Trace.WriteLine("HitTestCallback: dv = " + dv.ToString());

        foreach (Drawing drw1 in dv.Drawing.Children)
        {
            if (drw1 is GeometryDrawing)
            {
                GeometryDrawing drw = drw1 as GeometryDrawing;

                Trace.WriteLine("HitTestCallback: drw = " + drw.ToString());

                Trace.WriteLine("HitTestCallback: drw.Geometry = " + drw.Geometry.ToString());

                Trace.WriteLine("HitTestCallback: drw.Brush = " + drw.Brush.ToString());

                Trace.WriteLine("HitTestCallback: drw.IsFrozen = " + drw.IsFrozen.ToString());
                Trace.WriteLine("HitTestCallback: drw.Brush.IsFrozen = " + drw.Brush.IsFrozen.ToString());

                // does not work
                //SolidColorBrush newBrush = new SolidColorBrush(Colors.Red);

                //newBrush.Freeze();

                //drw.Brush = newBrush;

                // works with drw.Brush already containing a SolidColorBrush
                (drw.Brush as SolidColorBrush).Color =
                    (drw.Brush as SolidColorBrush).Color == Colors.LightGoldenrodYellow ?
                    Colors.Red : Colors.LightGoldenrodYellow;

                Trace.WriteLine("HitTestCallback: drw.Brush = " + drw.Brush.ToString());

                Trace.WriteLine("HitTestCallback: drw.IsFrozen = " + drw.IsFrozen.ToString());
                Trace.WriteLine("HitTestCallback: drw.Brush.IsFrozen = " + drw.Brush.IsFrozen.ToString());

            }
        }


    }

Just for the record I cite the code where the DrawingVisual is constructed and put in VisualHostClass

        PathGeometry pgnGeom = RenderPolygonWithHoles2Geometry(pgn);

        DrawingVisual dv = new DrawingVisual();

        using (DrawingContext dc = dv.RenderOpen())
        {
            SolidColorBrush brush1 = new SolidColorBrush(Colors.LightGoldenrodYellow);
            dc.DrawGeometry(brush1, null, pgnGeom);
        }

        myVisualHost.AddVisual(dv);
Jürgen Böhm
  • 389
  • 3
  • 11
  • Your code works fine for me. The Brush changes and the UI shows a red Drawing after clicking. I would however strongly recommend not to change any properties of the visual tree in a hit-test callback. – Clemens Jan 20 '21 at 17:25
  • Although changing a Brush is not actually a modification of the visual tree, it is worth to keep in mind what's said in [Using a Hit Test Result Callback](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/graphics-multimedia/hit-testing-in-the-visual-layer?view=netframeworkdesktop-4.8#using-a-hit-test-result-callback): *... During the hit test results enumeration, you should not perform any operation that modifies the visual tree. Adding or removing an object from the visual tree while it is being traversed can result in unpredictable behavior.* – Clemens Jan 20 '21 at 17:30
  • For the general question how to change the color of a drawing, it would much simpler to use one of the high level shape classes, like Rectangle or Path, and attach a MouseDown event handler or an IsMouseOver Trigger in XAML. – Clemens Jan 20 '21 at 17:35
  • @Clemens Regarding your recommendation of high level shape classes like Path: I used Shape earlier to render a lot of polygons which were given by a PathGeometry (each polygon in its own Path which became a child of a Canvas). But it was a bit slow, and so I wanted to follow the advice of the WPF-Book to use more lightweight DrawingVisuals. Indeed the speed of rendering on zoom and pan improved noticably. – Jürgen Böhm Jan 20 '21 at 17:44
  • @Clemens I modified the code now, so that in the callback the found DrawingVisual dv is added to a List called changeStage which is processed at the end of the mouse button event handler by the modify-code which above resides in HitTestCallback and is now made a separate member-function. But it does still not work. Maybe it is the problem, because I put VisualHostClass as a child in a canvas, maybe I have to call an "update" for this canvas? – Jürgen Böhm Jan 20 '21 at 17:56
  • I just put it in a Grid or Canvas (does not matter), then add a DrawingVisual with a GeometryDrawing. Nothing more is needed. – Clemens Jan 20 '21 at 18:01
  • How many shapes do you have in mind? I would tend to template out viewmodels into ui in an itemscontrol which has a canvas as itemspanel for this sort of requirement. How long to render do you consider slow? Our map building app can render an awful lot of trees with what i consider reasonable performance. Each tree is a usercontrol which has several brushes and geometries plus a dropshadow. – Andy Jan 20 '21 at 18:14
  • @Andy The example I use currently has 30305 polygons. But 500,000-1,000,000 polygons can occur too. (Think of the tracks on very complicated and big PCB). – Jürgen Böhm Jan 20 '21 at 18:34
  • @Clemens I can not get it working, maybe I use the wrong way to bring my class VisualHostClass into play. Currently I write mainCanvas.Children.Add(myHostingVisual); where mainCanvas the name given in the xaml file to a certain Canvas embedded in a Viewbox embedded in a Grid embedded in a (the) Window. – Jürgen Böhm Jan 20 '21 at 18:48
  • @Clemens I still can not make the brush change visible and now I am totally run out of new ideas. Could you post the code that worked for you in an answer, so that maybe by comparison I can find the clue. – Jürgen Böhm Jan 21 '21 at 09:57

1 Answers1

1

It appears that the Drawing created by dc.DrawGeometry(...) is frozen, i.e. not modifiable.

Passing my own Drawing has worked for me:

using (var dc = dv.RenderOpen())
{
    dc.DrawDrawing(new GeometryDrawing(Brushes.LightGoldenrodYellow, null, pgnGeom));
}
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • Many, many thanks! (I somehow was too reluctant to try this out, because I was from the WPF-Book a bit under the impression, that it would hurt performance. But it is (at least noticably) not slower). You would really like the view how PCB-traces of a complicated artwork get red and yellow again on mouseclick... – Jürgen Böhm Jan 21 '21 at 22:19