26

I am trying to achieve an unusual use of an Adorner. When you mouse-over a RichTextBox, an Adorner (see diagram below) will appear above it, allowing you to add a list of strings to a ListBox contained in the Adorner. This is used for adding "tags" (à la Flickr) to the passage contained in the adorned element.

adorner diagram

Firstly: is this even possible?

Most examples of Adorners show how to override the Adorner's OnRender method to do trivial things like draw shapes. I was able to use this to render a set of rectangles that creates the gray border of the Adorner, which also resizes automatically if the height of RichTextBox increases due to additional lines text being added while the Adorner is displayed.

protected override void OnRender(DrawingContext drawingContext)
{
    SolidColorBrush grayBrush = new SolidColorBrush();
    grayBrush.Color = Color.FromRgb(153, 153, 153);

    // left
    drawingContext.DrawRectangle(grayBrush, null, new System.Windows.Rect(1, 1, 5, ActualHeight));
    // right
    drawingContext.DrawRectangle(grayBrush, null, new System.Windows.Rect(ActualWidth - 6, 1, 5, ActualHeight));
    //bottom
    drawingContext.DrawRectangle(grayBrush, null, new System.Windows.Rect(1, ActualHeight, ActualWidth - 2, 5));

    // for reasons unimportant to this example the top gray bar is rendered as part of the RichTextBox

}

However, adding controls is slightly more problematic. Generally speaking, WPF's adorner requires adding child controls in code rather than XAML. Using the technique described in DrawingContext adorner - possible to draw stackpanel?, I have learned how to add child controls (like a TextBox) to an Adorner without any problem within the Adorner's initializer.

The issue, however, is the placement of those controls within the Adorner.

If I could create a grid with a gray background and position it at the bottom of the Adorner, I should be good to go. I would assume (hope) that things like automatic resizing of the Adorner based on the changing size of that Grid as tags are added would then happen automatically.

In short, assuming this is possible, can anyone recommend a way of creating this lower tagging control area within the Adorner and positioning it relative to the bottom of Adorner (which may possibly have to resize as the RichTextBox content resizes)?

Community
  • 1
  • 1
Ryan Norbauer
  • 1,718
  • 2
  • 17
  • 26
  • You don't have to get your hands that dirty, take a look at [this CodeProject article](http://www.codeproject.com/Articles/54472/Defining-WPF-Adorners-in-XAML) that I've used in the past. It lets you define adorners in XAML, do bindings, etc. – Louis Kottmann Apr 03 '12 at 21:11
  • I had actually fiddled around with this approach for a few hours trying to come up with a way to implement the positioning described above, but I don't think that the code in the CodeProject example is sufficiently robust to accommodate what I want to do. For example, it is clipping any adorner I create to the boundaries of the adorned element (very odd given that z-index of adorners is always supposed to be on top). I tried testing things like binding the height of an element under the AdornedControl.AdornerContent to the ActualHeight of the Adorned element and it failed (silently) to do so. – Ryan Norbauer Apr 03 '12 at 23:08
  • Yes it does the clipping, I found that it clips to the layout not to the adorned element. As for your problem I'll have another look – Louis Kottmann Apr 04 '12 at 08:26
  • Je vous remercie, Baboon. :) I'm going to keep at it this afternoon myself and will report anything I learn. – Ryan Norbauer Apr 04 '12 at 20:33
  • It actually clips to the bounds of the Adorners layer. You can put an AdornerDecorator anywhere you want in your VisualTree, and when you request the adorner layer, that's the part of the tree where it gets the layer. if your AdornerDecorator has its ClipToBounds set to true, it's clipped to that rect. If you don't want clipping, put your AdornerDecorator at the window level and your adorners will have free reign over placement. (I needed the exact opposite... to put an AdornerDecorator explicitly around a panel which I wanted the adorners of the child controls clipped to. – Mark A. Donohoe Oct 05 '15 at 20:27

1 Answers1

52

Huzah! With the help of Ghenadie Tanasiev, I've got an answer.

Unlike most controls in WPF, adorners don't have any out-of-the-box way of assigning child elements (such as the controls I wanted to add). Without adding anything to adorners, you can only override their OnRender method and draw stuff within the DrawingContext that gets passed into it. To be honest, this fits probably 99% of use cases for adorners (stuff like creating drag handles around an object), but I needed to add some proper controls to my Adorner.

The trick to doing this is to create a VisualCollection and set your adorner as its owner by passing it into the constructor for the collection.

This is all described pretty comprehensively in this blog article. Unfortunately, my repeated Google searches weren't turning this article up until I knew to search for VisualCollection, thanks to Ghenadie's guidance.

This isn't mentioned in the article, but note that it is possible to combine the VisualCollection technique along with drawing in the OnRender method of the adorner. I'm using OnRender to achieve the side and top borders described in my diagram above and using VisualCollection to place and create the controls.

Edit: here is the source code from the mentioned blog post since it is no longer available:

public class AdornerContentPresenter : Adorner
{
  private VisualCollection _Visuals;
  private ContentPresenter _ContentPresenter;

  public AdornerContentPresenter(UIElement adornedElement)
    : base(adornedElement)
  {
    _Visuals = new VisualCollection(this);
    _ContentPresenter = new ContentPresenter();
    _Visuals.Add(_ContentPresenter);
  }

  public AdornerContentPresenter(UIElement adornedElement, Visual content)
    : this(adornedElement)
  { Content = content; }

  protected override Size MeasureOverride(Size constraint)
  {
    _ContentPresenter.Measure(constraint);
    return _ContentPresenter.DesiredSize;
  }

  protected override Size ArrangeOverride(Size finalSize)
  {
    _ContentPresenter.Arrange(new Rect(0, 0,
         finalSize.Width, finalSize.Height));
    return _ContentPresenter.RenderSize;
  }

  protected override Visual GetVisualChild(int index)
  { return _Visuals[index]; }

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

  public object Content
  {
    get { return _ContentPresenter.Content; }
    set { _ContentPresenter.Content = value; }
  }
}
torvin
  • 6,515
  • 1
  • 37
  • 52
Ryan Norbauer
  • 1,718
  • 2
  • 17
  • 26