Custom content inside shape

Oct 1, 2012 at 12:01 PM

Hi,

Can I place not only text inside shape but some other content as well? Or (this would be ideal) can I place UserControl there?

Thank you.

Coordinator
Oct 2, 2012 at 7:23 AM

The shipped implementations do not support WinForm controls inside shapes but you can implement a WinFormsHostShape by deriving your own custom shape and your own user control:

  • Create a WinForms user control that has a public Draw method that can be called from other classes.
    The important point is that the user control draws itself to a given System.Drawing.Graphics object by caling OnPaint with PaintEventArgs constructed with the given Graphics context.
  • Pick a shape from the shipped shape libraries that suits your needs. Use this shape as base class for your shape.
    For simplicity, add your custom shape to the existing library and insert the register call into the library's NShapeInitializer
  • Override all methods responsible for calculating and drawing the shape's visuals.

In your case, I would advise to take the code of the ImageBasedShape as base for your new shape because it has a rather simple implementation and it's not rotatable (rotating WinForms UserControls might get a little tricky). Replace all the image related stuff with code that resizes/draws your user control.

This thread contains a sample how to create your own shape library and this thread provides a short description how to calculate the shape's geometry.

Oct 2, 2012 at 8:43 AM

Thank you for the reply.

I will try the approach you recommended.

One question more: is it possible to show in the PropertyGrid not only standard properties of the shape, but also some properties of my UserControl? For example, if a shape corresponds to a Windows Service, then I would like to create a UserControl, which will have such properties as Computer, StartupPath, StartupMode, LoginAccount etc. So, when user selects such shape in the designer, he/she must be able to edit these properties in the "property browser" (PropertyGrid). Currently this property browser allows to edit only "Text" property (standard property of any shape).

Coordinator
Oct 2, 2012 at 9:25 AM

The property grid simply shows the shape's properties (because it's the standard property grid that shows the given object's properties).
Your options:

  • Due to the fact that you plan to derive your own custom shapes, you could simply add all these properties to your shapes.
  • You could define an interface that contains all properties you want to edit and add a single property of that interface to your shape. Then, implement a System.Drawing.Design.UITypeEditor for your interface and register it as default editor (see Styles.cs, IStyle interfaces).
  • You could use the IModelObject interface for your purposes because the property presenter also supports displaying a second property grid for properties of model objects attached to the shapes. If your internal objects implement the IModelObject interface, you can use the standard PropertyPresenter component for editing these properties. See "ModelMapping Demo" sample program and "Model Mappings" sample project.
Oct 2, 2012 at 1:21 PM

Thank you! The last variant seems to be perfect: I was also thinking about separating appearance/layout-related properties from the properties of the model objects. And you've just offered the solution!

Oct 4, 2012 at 8:07 AM
KurtHolzinger wrote:

In your case, I would advise to take the code of the ImageBasedShape as base for your new shape because it has a rather simple implementation and it's not rotatable (rotating WinForms UserControls might get a little tricky). Replace all the image related stuff with code that resizes/draws your user control.

I derived a class from ImageBasedShape. The latter has constructor which differs from other base shape classes: it has 4 parameters instead of 2. These two additional parameters are resourceBaseName and resourceAssembly. As far as I'm not going to draw any image, I tried to pass null for these parameters, but it gives me an error.

Can I instead derive from another base class instead? Or how can I avoid that error?

Coordinator
Oct 4, 2012 at 9:29 AM

When I wrote "take the code of ImageBasedShape as base" I thought more of "copy the code of ImageBasedShape to your project and modify it"... ;-)

There is pretty much image related code inside the ImageBasedShape and as you will never need this code, just use the ImageBasedShape's code as code skeleton for your own implementation.
Feel free to derive your shape from other shapes but most of the shapes support rotating (in fact the ImageBasedShape is the only one not (yet) supporting rotation). Rotating the shape adds pretty much complexity to your shape code, especially if you want to display WinForm controls that are not rotatable by default...

Concerning the additional constructor parameters I think you will need something similiar but WinForm Control related, e.g. the control class and its library (or something like that). Maybe you will never need tham, depending on your implementation.

Oct 4, 2012 at 11:07 AM
Edited Oct 4, 2012 at 11:09 AM

Thank you for clarification. I copied ImageBasedShape class to my project but it doesn't compile: 2 lines of code produce error "cannot change access modifiers when overriding 'protected' inherited member '...'". These lines are:

protected internal override void InitializeToDefault(IStyleSet styleSet)

and

protected internal override int ControlPointCount

That's strange because I've changed nothing in this class. And access modifier is not changed in any of these lines: it's the same as in ShapeBase class.

Any ideas?!

Coordinator
Oct 4, 2012 at 11:26 AM

These methods are defined as "protected internal" by the base class inside the Dataweb.NShape namespace. If you adopt them to your own namespace you have to remove the "internal" declaration because a method cannot be internal in two namespaces at once.

protected override void InitializeToDefault(IStyleSet styleSet)
protected override int ControlPointCount
Oct 5, 2012 at 7:57 AM
KurtHolzinger wrote:
  • Create a WinForms user control that has a public Draw method that can be called from other classes.
    The important point is that the user control draws itself to a given System.Drawing.Graphics object by caling OnPaint with PaintEventArgs constructed with the given Graphics context.

Could you please give some clarification? Do you mean the following? -

In my shape code:

/// <override></override>
public override void Draw(Graphics graphics)
{
    if (graphics == null) throw new ArgumentNullException("graphics");
    UpdateDrawCache();
    _ctl.Draw(graphics); // here _ctl is my UserControl
    /*
    if (h >= ch + minH)
    {
        graphics.DrawImage(image, x - w / 2, Y - w / 2, w, h - ch);
        caption.Draw(graphics, CharacterStyle, ParagraphStyle);
    }
    else
        graphics.DrawImage(image, x - w / 2, Y - w / 2, w, h);
    */
    base.Draw(graphics);
}

In my UserControl code:

public void Draw(Graphics g)
{
    this.OnPaint(new PaintEventArgs(g, Rectangle.Round(g.ClipBounds)));
}

protected override void OnPaint(PaintEventArgs e)
{
    base.OnPaint(e);
    e.Graphics.DrawRectangle(Pens.Blue, e.ClipRectangle);
}

Coordinator
Oct 5, 2012 at 8:50 AM

Yes, something like that. There might be other solutions but this was the first one that came in my mind.

Oct 5, 2012 at 9:48 AM
Edited Oct 8, 2012 at 5:19 AM

I have one suggestion regarding the future development of your framework:

Please include in your standard libraries a shape which (1) is not rotatable, (2) has no any own visuals (neither text nor image/background) and (3) is fully drawn by the associated UserControl. This will provide a great flexibility! smth. like the ContentTemplate concept in WPF :)

[UPDATE] regarding item (3): well, actually there's no any great need in having UserControl: this shape can simply have abstract Draw(Graphics, Rectangle) method where we can do anything.

Oct 8, 2012 at 5:14 AM

I found it difficult to create my own shape based on the ImageBasedShape: most of the connection points disappeared :)

However, I derived my shape from Dataweb.NShape.GeneralShapes.Box and used the following code to disable rotation:

public override IEnumerable<ControlPointId> GetControlPointIds(ControlPointCapabilities controlPointCapability)
{
    foreach (var pointId in base.GetControlPointIds(controlPointCapability))
    {
        if (HasControlPointCapability(pointId, ControlPointCapabilities.Rotate))
            continue;
        yield return pointId;
    }
}

Just wanna know Your opinion: is this a valid code? Well, it seems to be working (rotation grip isn't drawn anymore, and I can't rotate the shape), but... maybe I need to do smth. else?!

Coordinator
Oct 9, 2012 at 7:15 AM
  • The right place to disable the rotation capability would be the HasControlPointCapability method:
    public override bool HasControlPointCapability(ControlPointId controlPointId, ControlPointCapabilities controlPointCapability) { 
        
    if (controlPointId == 9 && (controlPointCapability & ControlPointCapabilities.Resize) != 0) 
            
    return false;
        else return base
    .HasControlPointCapability(controlPointId, controlPointCapability);
    }
  • You should also hide (or otherwise disable) the Angle property
Oct 9, 2012 at 7:45 AM

Thank you very much!

With this approach the central point remains visible (although we can't use it for rotation anymore). Is there a way to hide it?

(in my approach this point was hidden - but it was the wrong approach as you said)

Coordinator
Oct 9, 2012 at 8:25 AM
Edited Oct 9, 2012 at 8:40 AM

If you want to disable other capabilities, just add them to the if clause:

public override bool HasControlPointCapability(ControlPointId controlPointId, ControlPointCapabilities controlPointCapability) { 
    
if (controlPointId == 9 && (controlPointCapability & (ControlPointCapabilities.Rotate | ControlPointCapabilities.Connect)) != 0) 
        
return false;
    else return base
.HasControlPointCapability(controlPointId, controlPointCapability);
}

Your approach should work in most (if not all) cases but the following code will fail:
            bool isRotatePoint = shape.HasControlPointCapability(9, ControlPointCapabilities.Rotate);
            ControlPointId controlPointId = ControlPointId.None;
            foreach (ControlPointId id in shape.GetControlPointIds(ControlPointCapabilities.Rotate)) {
                controlPointId = id;
                break;
            }
            System.Diagnostics.Debug.Assert((controlPointId == 9) == isRotatePoint);

In order to keep the behavior consistent, you should disable the capabilities. The rest is done by the framework.

Oct 9, 2012 at 8:31 AM

Unfortunately your last variant does not hide the central point :( maybe you were meaning ".Rotate" instead of ".Connect"?

Coordinator
Oct 9, 2012 at 8:43 AM

Oops... should be
(
ControlPointCapabilities.Rotate | ControlPointCapabilities.Connect)
instead of
(ControlPointCapabilities.Resize | ControlPointCapabilities.Connect)
Code corrected in the sample above.