Question Regarding SubGraph Decorations

Hello Again,

I am searching for a way to display a subgraph decorated as follows:

To the right of the subgraph’s expand/collapse button is what I call a lamp. The lamp, (currently showing black) represents the status of the things in the subgraph and should display a solid (or gradient) color from a palette: Red, Green, Yellow, etc… The lamp should remain visible, regardless of whether the subgraph is collapsed or expanded. To the right of the lamp is the subgraph’s label.

To accomplish what I want to do I was thinking that I could replace the subgraph label with a class consisting of a gonode with two children: one gotext and one goellipse, but the label method only returns a gotext

Is it possible to insert another object (goellipse) in between the subgraph’s expand/collapse indicator and the subgraph’s label?
Would it just be a matter of coding LayoutLabel properly? Or, are there other issues to consider?

Thoughts, comments?

Thanks a lot.

R. Houston

Have you looked at the CollapsedObject Property and the LayoutCollapsedObject Method on GoSubGraph?

This will get your collapsedObject in the right spot:

public override void LayoutCollapsedObject() {
GoObject icon = this.CollapsedObject;
if (icon == null)
return;
RectangleF r;
if (this.IsExpanded) {
base.LayoutCollapsedObject();
}
else
{
icon.SetSpotLocation(GoObject.TopLeft, this.Handle, GoObject.TopRight, 10, 0);
}
}
and LayoutLabel is called after this, so you can layout the label relative to the CollapsedObject.

Hi Jake,

Let’s see if I understand what you’re saying.

The lamp becomes the collapsed object (this.CollapsedObject), right? If the subgraph is collapsed then the lamp’s position is set relative to the SubGraph expand/collapse handle and since LayoutLabel occurs after LayoutCollapsedObject I can get the look I want. Sounds good. But, what about when the SubGraph is not collapsed? What does base.LayoutCollapsedObject do when the SubGraph is expanded? Or, am I missing something.

You’ve got it.

My sample just calls the base in the case it isn't expanded. I think it puts the collapsed object at topleft in that case, but it isn't visible.

Hi Jake,

I implemented your example code and it works for the case when the subgraph is collapsed. Unfortunately, I wanted the lamp visible even when the subgraph is expanded. Any ideas on how to get the lamp positioned and shown when the subgraph is expanded?

Thanks.

R. Houston

OK... here it is.

[code] // A GoSubGraph that adds a Lamp shape, positioned immediately to the
// right of the Handle, with the Label just to the right of the Lamp.
[Serializable]
public class LampSubGraph : GoSubGraph {
public LampSubGraph() {
// create a Lamp – could be any kind of object, including a group,
// but is currently assumed to be a GoShape
GoEllipse e = new GoEllipse();
e.Selectable = false;
e.Size = new SizeF(10, 10);
e.Pen = null;
e.BrushColor = Color.Black;
Add(e);
AddChildName(“Lamp”, e);
}

public GoShape Lamp {
  get { return FindChild("Lamp") as GoShape; }
}

// this is needed to handle the case when there are no child nodes--
// gotta have something when the subgraph is collapsed
protected override GoObject CreateCollapsedObject() {
  GoShape s = new GoRectangle();
  s.Visible = false;
  s.Printable = false;
  s.Pen = null;
  return s;
}

// ignore GoSubGraph.LabelSpot & CollapsedLabelSpot:
// layout Label in LayoutHandle instead, along with Handle and Lamp
public override void LayoutLabel() {}

// layout Handle, Lamp, and Label, all in a row outside of the inner area
public override void LayoutHandle() {
  GoSubGraphHandle hnd = this.Handle;
  GoShape lamp = this.Lamp;
  GoText lab = this.Label;
  RectangleF rect;
  if (this.IsExpanded) {
    rect = ComputeInsideMargins(null);
  } else {
    SizeF maxsize = ComputeCollapsedSize(true);
    rect = ComputeCollapsedRectangle(maxsize);
  }
  // allocate vertical room for all three objects
  float maxheight = this.TopLeftMargin.Height;
  if (hnd != null) maxheight = Math.Max(maxheight, hnd.Height);
  if (lamp != null) maxheight = Math.Max(maxheight, lamp.Height);
  if (lab != null) maxheight = Math.Max(maxheight, lab.Height);
  PointF p = new PointF(rect.X-this.TopLeftMargin.Width, rect.Y-maxheight);
  if (hnd != null) {
    // move Handle only when expanded
    if (this.IsExpanded) {
      hnd.Position = p;
    } else {
      p = hnd.Position;
    }
    p.X += hnd.Width + 3;  // spacing between Handle and Lamp
  }
  if (lamp != null) {
    lamp.Position = p;
    p.X += lamp.Width + 3;  // spacing between Lamp and Label
  }
  if (lab != null) {
    lab.Position = p;
  }
}

// the following overrides are for ignoring the new Lamp child object

protected override bool ComputeInsideMarginsSkip(GoObject child) {
  if (child == this.Lamp) return true;
  // need to skip Label too, since we're positioning it outside the inner area
  if (child == this.Label) return true;
 return base.ComputeInsideMarginsSkip(child);

}

protected override void SaveChildBounds(GoObject child, RectangleF sgrect) {
  if (child == this.Lamp) return;
  base.SaveChildBounds(child, sgrect);
}

protected override void CollapseChild(GoObject child, RectangleF sgrect) {
  if (child == this.Lamp) return;
  base.CollapseChild(child, sgrect);
}

}[/code]

Hi Jake,

Thank you very much for providing your code. I’ve implemented your example, above and it gets me closer to what I want but still some issues remain, mainly because I still don’t understand the finer points of how subgraphs work. Here are (some of) the issues that I need help with:

  1. I’ve coded my expanded subgraphs to appear as shadowed, rounded rectangles. Here are the specs:

    Me.Shadowed = True
    Me.BorderPen = Pens.Blue
    Me.Reshapable = True
    
    ' wide margin and large corners
    Me.Corner = New SizeF(40, 40)
    Me.TopLeftMargin = New SizeF(16, 16)
    Me.BottomRightMargin = New SizeF(16, 16)
    Me.BackgroundColor = Color.White
    Me.PickableBackground = True
    

What coding changes would I need to make to have the handle, lamp, and
label appear entirely inside (or outside) of the subgraph’s rounded rectangle?

  1. When one of my subgraphs is selected the user sees a green rectangular selection box around the subgraph object. How would I change the green selection box into a shape that is the same shape as the subgraph itself (i.e., rounded rectangle)?

  2. How would I display a rounded rectangle around the collapsed subgraph if I wanted one? After implementing your coding changes all I see is the handle, lamp, and label when I collapse the subgraph.

Thanks a lot for your help with these issues.

R. Houston

[code] // A GoSubGraph that adds a Lamp shape, positioned immediately to the
// right of the Handle, with the Label just to the right of the Lamp.
[Serializable]
public class LampSubGraph : GoSubGraph {
public LampSubGraph() {
// create a Lamp – could be any kind of object, including a group,
// but is currently assumed to be a GoShape
GoEllipse e = new GoEllipse();
e.Selectable = false;
e.Size = new SizeF(10, 10);
e.Pen = null;
e.BrushColor = Color.Black;
Add(e);
AddChildName(“Lamp”, e);
}

public GoShape Lamp {
  get { return FindChild("Lamp") as GoShape; }
}

// this is needed to handle the case when there are no child nodes--
// gotta have something when the subgraph is collapsed
protected override GoObject CreateCollapsedObject() {
  GoShape s = new GoRectangle();
  s.Visible = false;
  s.Printable = false;
  s.Pen = null;
  return s;
}

public override bool PaintsDecoration(GoView view) {
  return true;  // always paint the border, even though there's a CollapsedObject
}

// ignore GoSubGraph.LabelSpot & CollapsedLabelSpot:
// layout Label in LayoutHandle instead, along with Handle and Lamp
public override void LayoutLabel() {}

// layout Handle, Lamp, and Label, all in a row outside of the inner area
public override void LayoutHandle() {
  GoSubGraphHandle hnd = this.Handle;
  GoShape lamp = this.Lamp;
  GoText lab = this.Label;
  RectangleF rect;
  if (this.IsExpanded) {
    rect = ComputeInsideMargins(null);
  } else {
    SizeF maxsize = ComputeCollapsedSize(true);
    rect = ComputeCollapsedRectangle(maxsize);
  }
  PointF p = rect.Location;
  if (hnd != null) {
    // move Handle only when expanded
    if (this.IsExpanded) {
      hnd.Position = p;
    } else {
      p = hnd.Position;
    }
    p.X += hnd.Width + 3;  // spacing between Handle and Lamp
  }
  if (lamp != null) {
    lamp.Position = p;
    p.X += lamp.Width + 3;  // spacing between Lamp and Label
  }
  if (lab != null) {
    lab.Position = p;
  }
}

public SizeF ComputeHeaderSize() {
  GoSubGraphHandle hnd = this.Handle;
  GoShape lamp = this.Lamp;
  GoText lab = this.Label;
  float w = 0;
  float maxh = 0;
  if (hnd != null) {
    w += hnd.Width;
    maxh = Math.Max(maxh, hnd.Height);
  }
  if (lamp != null) {
    w += 3 + lamp.Width;  // include spacing too
    maxh = Math.Max(maxh, lamp.Height);
  }
  if (lab != null) {
    w += 3 + lab.Width;  // include spacing too
    maxh = Math.Max(maxh, lab.Height);
  }
  return new SizeF(w, maxh);
}

public override RectangleF ComputeInsideMargins(GoObject ignore) {
  SizeF hsize = ComputeHeaderSize();
  if (this.IsExpanded) {
    RectangleF r = base.ComputeInsideMargins(ignore);
    return new RectangleF(r.X, r.Y - hsize.Height, Math.Max(r.Width, hsize.Width), r.Height + hsize.Height);
  } else {
    PointF hpos = ComputeReferencePoint();
    return new RectangleF(hpos.X, hpos.Y, hsize.Width, hsize.Height);
  }
}

// the following overrides are for ignoring the new Lamp child object

protected override bool ComputeInsideMarginsSkip(GoObject child) {
  if (child == this.Lamp) return true;
  // need to skip Label too, since we're positioning it outside the inner area
  // where the nodes are
  if (child == this.Label) return true;
  return base.ComputeInsideMarginsSkip(child);
}

protected override void SaveChildBounds(GoObject child, RectangleF sgrect) {
  if (child == this.Lamp) return;
  base.SaveChildBounds(child, sgrect);
}

protected override void CollapseChild(GoObject child, RectangleF sgrect) {
  if (child == this.Lamp) return;
  base.CollapseChild(child, sgrect);
}

}[/code]

  1. OK, the Handle/Lamp/Label (now called the “header”) are now inside the border. Your screenshot didn’t show any border, so I assumed that you wanted everything outside the border.

  2. Override GoObject.CreateBoundingHandle to return what you want.

  3. That’s done by overriding GoSubGraph.PaintsDecoration to always return true, as above.

For simple “lamps”, such as the ellipse that you seem to have, you could easily implement a simpler solution than above by not creating and adding a shape object to the subgraph. Adding that object that is laid out specially and is not a child node or link complicates the implementation, as you can see by the additional overrides that were required.

An alternative is to just override Paint to draw the ellipse. That’s real easy for simple shapes for which you can easily call the desired Graphics method(s). But you’d need to implement a property to hold the color if you want to change the color dynamically.

However, using a real GoObject as the “lamp” gives you a lot more flexibility in both appearance and behavior. You could have arbitrarily complex objects instead, and they could be modified dynamically, including changing their sizes. For example you can easily change the color of the ellipse by:

lsg.Lamp.BrushColor = Color.Red

You could use a GoDrawing or a GoImage just as easily. On the other hand, implementing GoDrawing functionality in a Paint method would be hard.

Hi Walter,

I really appreciate your help with this solution.

When I first thought about an approach to solving my problem I thought about constructing an object consisting of a goellipse (lamp) and gotext (label) but I couldn’t find a way to tie this derived “header” class to the subgraph. That’s why I posted here. But, one of your comments sparked a thought for an enhancement to subgraphs… Provide a this.header property for the subgraph, much the way a subgraph provides a this.label property. The header class could either replace the label entirely, or work in conjunction with a label, depending on how best to implement this functionality. As you mention, the header could consist of any goobjects. Anyway, its just a thought - my two cents on the subject.

Lastly, I was wondering if a goellipse can be made to look 3D with shading and a gradient fill? I found the code for making a gradient rectangle and was thinking of adapting that to the ellipse but then I couldn’t find an approach for making the ellipse 3D. Does something like this exist?

Thanks again for your help.

In version 3.0, replace:
e.BrushColor = Color.Black;
with:
e.FillEllipseGradient(Color.Blue);
e.BrushPoint = new PointF(0.4f, 0.4f);

Just calling one of the GoShape.Fill… methods is the simplest way to get most of what you probably want.

Then there are a bunch of GoShape.Brush… properties you can play with to customize the effect you want, such as the BrushPoint, above.