Different appearance for shape's toolbox image and real shape

Oct 19, 2012 at 6:04 AM

Is it possible to have different appearance for shape's toolbox image and real shape?

I have a custom shape derived from PreparationSymbol and it is convenient to have its default size 250x80. So I used the following code to achieve this:

protected override void InitializeToDefault(IStyleSet styleSet)
{
    base.InitializeToDefault(styleSet);
    this.Width = 250;
    this.Height = 80;
}

However, the toolbox image looks ugly (too thin) - while the real shape (dropped on the Diagram's surface) looks OK.

Coordinator
Oct 19, 2012 at 6:54 AM
Edited Mar 6, 2013 at 10:48 AM
The method "DrawThumbnail" is responsible for creating the toolbox image. You can override and re-implement it if you want. You can even return a resource image although it will not respond to color changes in this case. [Update] If you override this method, create a clone of the shape before resizing (or otherwise modifying it) it. This clone has to be disposed after usage:
// ... 
using (Graphics g = Graphics.FromImage(image)) { 
    GdiHelpers.ApplyGraphicsSettings(g, RenderingQuality.MaximumQuality); 
    g.Clear(transparentColor); 
    // ... 
    // Dispose the shape's clone after usage! 
    using (MyShapeType myShape = (MyShapeType)this.Clone()) { 
        // Modify shape, create specific styles, etc 
        // ... 
        // Draw shape 
        myShape.Draw(gfx); 
    } 
}   
Oct 19, 2012 at 7:08 AM

Thank you.

I was also thinking about overriding this method, but after I read its description in the Help file ("This method is used by the display to mark selected shapes") I decided that this is not what I need.

Coordinator
Oct 19, 2012 at 7:18 AM

Oops... wrong description for the method. Thanks for reporting this issue, I will correct it.

Oct 19, 2012 at 11:54 AM
Edited Oct 22, 2012 at 8:47 AM

As I can see in the code, the last line of the DrawThumbnail() method is a call to the Draw() method. Should the Draw() method have an additional argument bool thumbnailMode, I would simply edit my Draw() method's override by adding two branches (if(thumbnailMode == true)...else...) and painting differently in these branches. But there is no such argument :(( so inside the Draw() method I do not know whether I'm drawing a "real" shape (for Display component) or a thumbnail (for Toolbox). As a result, I'll have to override the DrawThumbnail() method and in its last line call smth. instead of the Draw() method. It's not a real problem, just some inconvenience...

[UPDATE] BTW it's not possible to override the DrawThumbnail() method because of the following line of its code:

GdiHelpers.ApplyGraphicsSettings(g, RenderingQuality.MaximumQuality);

Unfortunately, the Dataweb.NShape.Advanced.GdiHelpers class is marked as internal! Of course we can duplicate it in our custom shapes library, but it's not a clean solution.

Oct 22, 2012 at 9:24 AM

Well, that's my final solution:

namespace MyShapes
{
    /// <summary>
    /// Describes behavior which is common for all of my shapes.
    /// </summary>
    internal static class MyShapesCommonBehavior
    {
        /// <summary>
        /// Copied from Dataweb.NShape.Advanced.ShapeBase.DrawThumbnail(), extended with parameters "shape" and "drawDelegate".
        /// </summary>
        /// <todo>### SYNC with Shapebase when new version gets released!</todo>
        public static void DrawThumbnail(ShapeBase shape, Action<Graphics, bool> drawDelegate, Image image, int margin, Color transparentColor)
        {
            if (image == null) throw new ArgumentNullException("image");
            using (Graphics g = Graphics.FromImage(image))
            {
                ApplyGraphicsSettings(g, RenderingQuality.MaximumQuality);
                g.Clear(transparentColor);

                Rectangle srcRectangle = shape.GetBoundingRectangle(true);
                Rectangle destRectangle = Rectangle.Empty;
                destRectangle.X = destRectangle.Y = margin;
                destRectangle.Width = image.Width - (2 * margin);
                destRectangle.Height = image.Height - (2 * margin);

                float scale = Geometry.CalcScaleFactor(srcRectangle.Width, srcRectangle.Height, destRectangle.Width, destRectangle.Height);
                g.ScaleTransform(scale, scale, MatrixOrder.Append);

                int dx = (int)Math.Round(((image.Width / scale) - srcRectangle.Width) / 2f);
                int dy = (int)Math.Round(((image.Height / scale) - srcRectangle.Height) / 2f);

                shape.MoveBy(-srcRectangle.X + dx, -srcRectangle.Y + dy);
                //shape.Draw(g); // that's the ONLY change in this method; instead we invoke the given delegate:
                drawDelegate.Invoke(g, true);
            }
        }

        /// <summary>
        /// Copied from Dataweb.NShape.Advanced.GdiHelpers class (because unfortunately this class is internal).
        /// See also http://nshape.codeplex.com/discussions/400008.
        /// </summary>
        /// <todo>### SYNC with GdiHelpers when new version gets released!</todo>
        private static void ApplyGraphicsSettings(Graphics graphics, RenderingQuality renderingQuality)
        {
            // method's code omitted to save space; that's simply an EXACT copy.
        }
    }
    
    ...
    
    public class ProcessorShape : PreparationSymbol, IMyShape
    {
        ...
        
        public override void Draw(Graphics graphics)
        {
            this.Draw(graphics, false);
        }

        public override void DrawThumbnail(Image image, int margin, Color transparentColor)
        {
            MyShapesCommonBehavior.DrawThumbnail(this, this.Draw, image, margin, transparentColor);
        }

        /// <summary>
        /// Parameter "thumbnailMode" is set to "true" when call is made from MyShapesCommonBehavior.DrawThumbnail() method,
        /// and is set to "false" - when from this.Draw(Graphics) overload.
        /// </summary>
        private void Draw(Graphics graphics, bool thumbnailMode)
        {
            base.Draw(graphics);
            if (thumbnailMode == false)
            {
                // it's not a thumbnail --> we must draw text:
                ProcessorModel pm = this.ModelObject as ProcessorModel;
                if (pm != null)
                {
                    Rectangle rect = this.GetBoundingRectangle(false);
                    rect.Inflate(-20, -10);
                    graphics.DrawString(string.Format("Type: {0}\r\nName: {1}", pm.UcsbModuleType, pm.UcsbArtifactName),
                        DrawingToolsCache.GetFont(this.CharacterStyle.FontFamily.Name, this.CharacterStyle.SizeInPoints, this.CharacterStyle.Style),
                        DrawingToolsCache.GetBrush(Color.Black),
                        rect);
                }
            }
        }
    }
}

Not ideal - but in my previous post I described a few limitations of the framework which pushed me to use such a solution.

Feb 28, 2013 at 9:30 AM
What about adding argument bool thumbnailMode to the Draw() method in future release? Can you discuss this?
Coordinator
Mar 1, 2013 at 7:14 AM
This wish was revoked.
Mar 5, 2013 at 8:40 AM
Sad to hear that.
OK, maybe adding argument to the Draw() method (via new overload, of course) is really not the best idea... but what about any other way to obtain different appearance for shape's toolbox image and real shape? I hope you will agree that toolbox image - if zoomed automatically - may look ugly... smth. must be done with that!
Coordinator
Mar 6, 2013 at 10:39 AM
Edited Mar 6, 2013 at 11:13 AM
I cannot see the problem - the toolbox image is drawn by calling Shape.DrawThumbnail(Image image, int margin, Color transparentColor).
You can draw whatever you want onto the given image. The default implementation draws a scaled version of the template's shape and most shapes leave this default implementation but this is not necessary. The text shapes (Text and Label) for example draw "ab" instead of a scaled shape, the linear shapes just draw a line (using the line shape's pen).
Coordinator
Mar 6, 2013 at 11:13 AM
Addendum:
Your wish was revoked because the interface is flexible enough to ensure that derived shapes can draw any image as toolbox image.

Btw: The size of the toolbox images depends on the toolbox's presenter components - namely the ListView's image collections in case of the ToolBoxListViewPresenter. If you set the ListView's View property to e.g. "LargeIcons", the images are drawn bigger.
Mar 7, 2013 at 9:50 AM
I wanted to use the default implementation of the DrawThumbnail() method - which "draws a scaled version of the template's shape" - BUT avoid drawing text because it looks ugly on a thumbnail. That's why I wanted to have the following logic in the Draw() method of my shape:
public override void Draw(Graphics graphics, bool thumbnailMode)
{
    this.DrawEverythingExceptText();
    if (thumbnailMode == false)
    {
        // it's not a thumbnail --> we must draw text:
        this.DrawText();
    }
}
But it's impossible :(
Well, I found a workaround (see code in the message dated Oct 22, 2012), but it's rather ugly.
Coordinator
Mar 14, 2013 at 10:42 AM
Edited Mar 14, 2013 at 10:43 AM
How about this one?
public override void DrawThumbnail(Image image, int margin, Color transparentColor) {
    using (Graphics g = Graphics.FromImage(image)) {
        // Set maximum rendering quality (TextRenderingHint is not needed in this case)
        g.SmoothingMode = SmoothingMode.HighQuality;
        g.InterpolationMode = InterpolationMode.HighQualityBicubic;
        g.CompositingQuality = CompositingQuality.HighQuality;
        // Fill the background with the transparency mask color
        g.Clear(transparentColor);
        using (MyShape s = (MyShape)this.Clone()) {
            // Adjust the shape (delete the default text in this case)
            s.Text = "";
            s.Draw(g);
        }
    }
}
Mar 14, 2013 at 10:55 AM
Thank you for the idea!
A couple of days ago I also got another one (but haven't tested it yet): shapes located in the ToolBox - and so drawn with DrawThumbnail() and not with Draw() - have their Diagram property set to null. So I guess I can write smth. like this (instead of my last code snippet):
public override void Draw(Graphics graphics)
{
    this.DrawEverythingExceptText();
    bool thumbnailMode = (this.Diagram == null);
    if (thumbnailMode == false)
    {
        // it's not a thumbnail --> we must draw text:
        this.DrawText();
    }
}
What do you think?
Coordinator
Mar 14, 2013 at 11:53 AM
Caution:
The Shape.Diagram property is also null for all child shapes in Shape.Children...
Mar 14, 2013 at 12:16 PM
Then
bool thumbnailMode = (this.Diagram == null && this.Parent == null);
???

Is Parent property assigned for members of Children collection?
Coordinator
Mar 14, 2013 at 4:13 PM
This should work for all cases.
Yes, the Parent property is assigned for the members of the Children collection.
Mar 16, 2013 at 3:06 PM
Great!
However, I'm still sure that the Shape type should expose some protected field(s) which will allow us to distinguish "real" shape from "preview" shape and from "toolbox" shape. The developer of a custom shape may wish to draw shape differently in these three cases! at least so do I :) and maybe not only draw - maybe the behavior of the shape will also differ... who knows... some freedom for the developer will be nice to have! I don't think that exposing a protected read-only field can be dangerous :)
Coordinator
Mar 18, 2013 at 9:47 AM
The 'ToolBox Shape' can be identified by
bool isToolBoxShape = (myShape.Template.Shape == myShape);

When creating a preview shape, a shape is cloned first and afterwards the clone's "MakePreview" method is called.
So if you want to have any properties telling you if your shape is a preview shape, you can implement this easyly.
To be honest, I don't think we will implement this in the framework's base classes...
Mar 18, 2013 at 10:14 AM
Edited Mar 18, 2013 at 12:04 PM
KurtHolzinger wrote:
The 'ToolBox Shape' can be identified by
bool isToolBoxShape = (myShape.Template.Shape == myShape);
Isn't your isToolBoxShape variable the same as my thumbnailMode variable?! several posts above you've wrote that "the toolbox image is drawn by calling Shape.DrawThumbnail(...)"

[UPDATE] If in one of my custom shapes I write the following
public override void Draw(Graphics graphics)
{
    base.Draw(graphics);
    bool isToolBoxShape = (this.Template.Shape == this);
    if (isToolBoxShape == false)
    {
        // it's not a thumbnail --> we must draw text:
        // ...
    }
}
-- then I get NullReferenceException because this.Template is null. Here's the stack trace:

UcsbStudio.Shapes.dll!UcsbStudio.Shapes.ComputerShape.Draw(System.Drawing.Graphics graphics) Line 109 + 0x11 bytes C#
Dataweb.NShape.dll!Dataweb.NShape.Advanced.ShapeBase.DrawThumbnail(System.Drawing.Image image, int margin, System.Drawing.Color transparentColor) Line 813 + 0xe bytes C#
Dataweb.NShape.dll!Dataweb.NShape.TemplateTool.RefreshIcons() Line 3485 + 0x36 bytes C#
Dataweb.NShape.dll!Dataweb.NShape.TemplateTool.TemplateTool(Dataweb.NShape.Template template, string category) Line 3506 + 0xb bytes C#
Dataweb.NShape.dll!Dataweb.NShape.PlanarShapeCreationTool.PlanarShapeCreationTool(Dataweb.NShape.Template template, string category) Line 4245 + 0xe bytes C#
Dataweb.NShape.dll!Dataweb.NShape.Controllers.ToolSetController.CreateTemplateTool(Dataweb.NShape.Template template, string categoryTitle) Line 216 + 0x1b bytes C#
Dataweb.NShape.dll!Dataweb.NShape.Controllers.ToolSetController.Repository_TemplateInserted(object sender, Dataweb.NShape.RepositoryTemplateEventArgs e) Line 736 + 0x21 bytes C#
Dataweb.NShape.dll!Dataweb.NShape.Advanced.CachedRepository.InsertAll(Dataweb.NShape.Template template) Line 955 + 0x44 bytes C#
Dataweb.NShape.dll!Dataweb.NShape.Project.CreateDefaultTemplate(Dataweb.NShape.Advanced.ShapeType shapeType) Line 1059 + 0x17 bytes C#
Dataweb.NShape.dll!Dataweb.NShape.Project.Dataweb.NShape.Advanced.IRegistrar.RegisterShapeType(Dataweb.NShape.Advanced.ShapeType shapeType) Line 505 + 0x3b bytes C#
UcsbStudio.Shapes.dll!UcsbStudio.Shapes.NShapeLibraryInitializer.Initialize(Dataweb.NShape.Advanced.IRegistrar registrar) Line 29 + 0xcc bytes C#
[External Code]
Dataweb.NShape.dll!Dataweb.NShape.Project.InitializeLibrary(Dataweb.NShape.Project.Library library) Line 1042 + 0x1f bytes C#
Dataweb.NShape.dll!Dataweb.NShape.Project.DoRegisterLibrary(Dataweb.NShape.Project.Library library, bool adding) Line 876 + 0xb bytes C#
Dataweb.NShape.dll!Dataweb.NShape.Project.AddLibrary(System.Reflection.Assembly assembly, bool unloadOnClose) Line 341 + 0x35 bytes C#
MyApp.exe!MyApp.MainForm.CreateProject() Line 555 + 0x43 bytes C#


KurtHolzinger wrote:
When creating a preview shape, a shape is cloned first and afterwards the clone's "MakePreview" method is called.
So if you want to have any properties telling you if your shape is a preview shape, you can implement this easyly.
That's exactly what I'm doing now:
[Browsable(false)]
public bool IsPreview { get; set; }

public override void MakePreview(IStyleSet styleSet)
{
    base.MakePreview(styleSet);
    this.IsPreview = true;
}
KurtHolzinger wrote:
To be honest, I don't think we will implement this in the framework's base classes...
:(((
Then at least there should be a separate note in the Help file describing how one can distinguish the three cases (real shape, preview shape, toolbox shape)!
Apr 8, 2013 at 3:05 PM
Up!
Coordinator
Apr 12, 2013 at 8:00 AM
Comanche wrote:
Isn't your isToolBoxShape variable the same as my thumbnailMode variable?!
In your case: Yes, I think so.
In general: No.
bool thumbnailMode = (this.Diagram == null && this.Parent == null);
This is true for every shape that is not part of a diagram and is not part of an aggregated shape or a group.
It would also be true for (unaggregated) shapes removed from the diagram and stored in some list for later usage.
It does not really matter as they will not be drawn in this case but it is not the same as

On the other hand, the "isToolBoxShape" will only work for determining if the shape is not the template (toolbox) shape (as the template's shape has no template assigned). This is the reason why you are getting the NullReferenceException.
This should work in most cases:
bool isToolBoxShape = (this.Template == null || this.Template.Shape == this);
It does not work as expected if your code creates shapes without template.
I will add a note to the documentation how to draw custom toolbox images and how to distinguish the three kind of shapes.
Apr 12, 2013 at 10:37 AM
Thank you very much for the explanations!