How to Override Children.Add method of Canvas class in WPF - c#

I already have a Custom Canvas class in which i have override the 'OnMoseDown' method.
But now i also want to override 'Children.Add' method.
But i didnt find any solution.
I want to get Child.ActualWidth when a child gets added to canvas.
Thanks.

I just found my answer by myself. Hope this may help someone on some day.
Just added a Add method in my Custom class
public void Add(UserControl element)
{
bool alreadyExist = false;
element.Uid = element.Name;
foreach (UIElement child in this.Children)
{
if (child.Uid == element.Uid)
{
alreadyExist = true;
this.BringToFront(child);
}
}
if (!alreadyExist)
{
this.Children.Add(element);
this.UpdateLayout();
double top = (this.ActualHeight - element.ActualHeight) / 2;
double left = (this.ActualWidth - element.ActualWidth) / 2;
Canvas.SetLeft(element, left);
Canvas.SetTop(element, top);
}
}

This will likely not work correctly. A child will get added before it's ActualWidth is set, as that property gets set during the layout phase.
Depending on your goals here, you may want to consider overriding MeasureOverride and ArrangeOverride instead, and making a custom layout panel instead of subclassing Canvas.

Related

Get UI element Size from Canvas in WPF Forms

I instantiate new Ui elements onto a canvas like so:
public class MainForm :Canvas
{
List<BannerImage> bannerList;
AddImages()
{
bannerImage = new BannerImage("title", "content");
//accompanied with animation
Children.Add(bannerImage);
bannerList.Add(bannerImage);
}
I need to call the bannerImages to get their current position, the following works:
foreach(bannerItem in bannerList)
{
double rightPosition = Canvas.GetRight(bannerItem);
}
But I can't do the following:
bannerItem.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)
Size s = bannerItem.DesiredSize;
Which always ends up to be
{0,0}
Why is it that I can get the position of the item on the canvas but not the size?
I am just going to take a guess that you didn't override MeasureOverride. I will provide a basic implementation assuming that each element is stacked, but you would need to modify it to take into consideration your child controls and what ever custom layout you may have created (I don't know if they are in a grid, horizontally stacked, in a some kind of scrolled container, etc).
protected override Size MeasureOverride(Size availableSize)
{
var height = 0.0;
var width = 0.0;
foreach (UIElement child in InternalChildren)
{
child.Measure(availableSize);
if (child.DesiredSize.Width > width) width = child.DesiredSize.Width;
height += child.DesiredSize.Height;
}
width = double.IsPositiveInfinity(availableSize.Width) ? width : Math.Min(width, availableSize.Width);
height = double.IsPositiveInfinity(availableSize.Height) ? height : Math.Min(height, availableSize.Height);
return new Size(width, height);
}
Edit
I realized that I explained the issue in a comment, but didn't add it into my answer. The reason you can't get the size is because you have to provide an override in your derived class to compute it. By default, Canvas returns a DesiredSize of 0 since it will adapt to whatever size is assigned to it. In the case of your derived control, you have a Canvas as the base class but you have added additional controls to it. If you don't provide an override of the MeasureOverride method, then the base one (the one implemented by Canvas) is the only one that is called. The base Canvas knows nothing of your controls size requirements. You probably also will need to override ArrangeOverride. This article provides a pretty good explanation about the two methods, what they do and why you need to override them. It also provides and example of both methods.

ScrollViewer doesn't scroll for custom panel

I am making my own custom panel, which is supposed to scroll vertically when the content does not fit the available space, so i put it in a ScrollViewer.
Right now i can't get the ScrollViewer to activate the scrollbar when the panel inside is bigger then the ScrollViewer itself.
The permille functions get attached properties telling how big the childs have to be compared to the available size (without scrolling), aka the ViewPort.
As the size passed in MeasureOverride passes infinite, i don't think i can use the permille functions there.
That is why i measure my children in ArrangeOverride (not best practice, i guess) but that way the scrollviewer doesn't scroll.
How do i get this to work?
My XAML code:
<ScrollViewer>
<controls:TilePanel x:Name="TilePanel" PreviewMouseLeftButtonDown="TilePanel_PreviewMouseLeftButtonDown" PreviewMouseLeftButtonUp="TilePanel_PreviewMouseLeftButtonUp"
PreviewMouseMove="TilePanel_PreviewMouseMove" DragEnter="TilePanel_DragEnter" Drop="TilePanel_Drop" AllowDrop="True" />
</ScrollViewer>
My Custom Panel Class:
/// <summary>
/// A Panel Showing Tiles
/// </summary>
public class TilePanel : PermillePanel
{
public TilePanel()
{
}
protected override Size MeasureOverride(Size constraint)
{
//here constraint width or height can be infinite.
//as tiles are a permille of that height, they too can be infinite after measuring
//this is unwanted behavior, so we measure in the ArrangeOverride method
if (constraint.Width == double.PositiveInfinity)
{
return new Size(0, constraint.Height);
}
else if (constraint.Height == double.PositiveInfinity)
{
return new Size(constraint.Width, 0);
}
else
{
return constraint;
}
}
protected override Size ArrangeOverride(Size arrangeSize)
{
//return base.ArrangeOverride(arrangeSize);
foreach (FrameworkElement child in InternalChildren)
{
Size availableSize = new Size();
//set the width and height for the child
availableSize.Width = arrangeSize.Width * TilePanel.GetHorizontalPermille(child) / 1000;
availableSize.Height = arrangeSize.Height * TilePanel.GetVerticalPermille(child) / 1000;
child.Measure(availableSize);
}
// arrange the children on the panel
// fill lines horizontally, when we reach the end of the current line, continue to the next line
Size newSize = new Size(arrangeSize.Width, arrangeSize.Height);
double xlocation = 0;
double ylocation = 0;
double ystep = 0;
double maxYvalue = 0;
foreach (FrameworkElement child in InternalChildren)
{
double endxlocation = xlocation + child.DesiredSize.Width;
double constrainedWidth = arrangeSize.Width * TilePanel.GetHorizontalPermille(child) / 1000;
double constrainedHeight = arrangeSize.Height * TilePanel.GetVerticalPermille(child) / 1000;
if (TilePanel.GetVerticalPermille(child) != 0 && TilePanel.GetHorizontalPermille(child) != 0)
{
//horizontal overflow -> next line
if (endxlocation >= this.DesiredSize.Width *1.01)
{
ylocation += ystep;
xlocation = 0;
}
}
Rect rect = new Rect(xlocation, ylocation, constrainedWidth, constrainedHeight);
child.Arrange(rect);
xlocation += constrainedWidth;
ystep = Math.Max(ystep, constrainedHeight);
maxYvalue = Math.Max(maxYvalue, ystep + constrainedHeight);
}
if (maxYvalue > newSize.Height)
{
newSize.Height = maxYvalue;
}
return newSize;
}
}
Calling Measure() from within ArrangeOverride() will cause problems. The framework detects this and forces a remeasure. Set a tracepoint in MeasureOverride(), and I'll bet you'll see that it keeps getting called over and over again, even though the layout hasn't changed1.
If you absolutely have to call Measure() from ArrangeOverride(), you will need to do so conditionally such that it only forces a remeasure when the available size actually changes since the last call to Measure(). Then, you'll effectively end up with two measure + arrange passes any time the layout is invalidated, as opposed to just one. However, such an approach is hacky, and I would advise sticking to the best practice of only measuring within MeasureOverride().
1Interestingly, your UI may still respond to input, despite this apparent "infinite loop" in the layout.
If you want to use a custom Panel inside a ScrollViewer then you must add the code that does the actual scrolling. You can do that by implementing the IScrollInfo Interface in your custom Panel.
You can find a tutorial that explains this interface and provides an example code imeplementation in the WPF Tutorial - Implementing IScrollInfo page on the Tech Pro website. It's a fairly simple procedure and looks a tiny bit like this:
public void LineDown() { SetVerticalOffset(VerticalOffset + LineSize); }
public void LineUp() { SetVerticalOffset(VerticalOffset - LineSize); }
public void MouseWheelDown() { SetVerticalOffset(VerticalOffset + WheelSize); }
public void MouseWheelUp() { SetVerticalOffset(VerticalOffset - WheelSize); }
public void PageDown() { SetVerticalOffset(VerticalOffset + ViewportHeight); }
public void PageUp() { SetVerticalOffset(VerticalOffset - ViewportHeight); }
...

Determining which CustomPopupPlacement was used for WPF Popup

I'm trying to figure out which of the passed in array of CustomPopupPlacement positions have been used when the popup actually renders. Is there any event to detect this?
This msdn thread from 2009 seems to be exactly my issue however there does not seem to be an answer for it.
http://social.msdn.microsoft.com/Forums/da/wpf/thread/4c6d216a-0011-4202-aa7e-2fccef3cc355
The marked answer seems invalid and my situation is exactly as the OP in the thread.
I'm going to have my popup with 4 paths and use a DP to toggle visibility on three paths to choose the correct arrow path being rendered.
So given we provide 4 placement options via the CustomPopupPlacementCallbackdelegate, Is there a way to detect which of the 4 positions the system finally chose after dealing with screen edge cases and the sorts.
A better way to find placement of the Popup.
This method requires a Child element to be present, but that's no problem considering the Grid that comes with a Popup element.
UIElement container = VisualTreeHelper.GetParent(this) as UIElement;
Point relativeLocation = this.Child.TranslatePoint(new Point(0, 0), container); //It HAS(!!!) to be this.Child
if (relativeLocation.Y < 0) //or use X for left and right
{
Console.WriteLine("TOP PLACEMENT!");
}
else
{
Console.WriteLine("BOTTOM PLACEMENT!");
}
I have a little hacky solution.
Save the custom points as fields in the derived TooTip class and override the OnOpened method.
protected override void OnOpened(RoutedEventArgs e)
{
base.OnOpened(e);
var p = this.TranslatePoint(new Point(0, 0), this.PlacementTarget);
var diff1 = this.first - p;
var diff2 = this.second - p;
if (Math.Abs(Math.Min(diff1.Length, diff2.Length) - diff1.Length) < 0.01)
{
// First Point
}
else
{
// Second Point
}
}
Better solutions are welcome

Weifenluo Dock Panel Suite: Float windows using their design size?

How can I make floating windows use their design size (and not the Dock Panel Suite's default size) with the Weifenluo Dock Panel suite?
Hint: I tried a proposition from the Dock Panel Suite forums at SF.net, but that doesn't seem to work.
I stumbled across this question when looking for the answer myself, and found Timothy's answer to not work for me.
The problem was that the method he outlines also floated the window by default. (maybe that's a version difference)
I have solved this another way. I've created a base class that inherits from DockContent that all my document windows would inherit from. I then created another overload for the Show method that handles this (I used the DockPanelSuite source code to help build this method).
public void Show(DockPanel dockPanel, DockState dockState, Rectangle floatWindowBounds)
{
Show(dockPanel, dockState); //shows the panel like normal
//now for the part to initialize the float pane and size
if (DockHandler.FloatPane == null)
{
DockHandler.FloatPane = dockPanel.DockPaneFactory.CreateDockPane(this, DockState.Float, false);
DockHandler.FloatPane.FloatWindow.StartPosition = System.Windows.Forms.FormStartPosition.Manual;
}
DockHandler.FloatPane.FloatWindow.Bounds = floatWindowBounds;
}
when CForm is derived from DockContent,
I have a method within my MDIContainerWindow which looks like this
public void ShowForm(CForm pForm)
{
pForm.MdiParent = this;
Size lS = pForm.Size;
dockPanel.DefaultFloatWindowSize = lS;
pForm.Show(dockPanel);
pForm.VisibleState = DockState.Float;
}
This is working for me (in VB):
Dim MyForm As New MyForm
MyForm.Show(DockPanel, New Rectangle(MyForm.Location, MyForm.Size))
MyForm.DockState = DockState.DockRight
This worked for me:
var topLeft = dockPanel1.Location;
topLeft.X += (dockPanel1.Size.Width / 2 - newForm.Size.Width / 2);
topLeft.Y += (dockPanel1.Size.Height / 2 - newForm.Size.Height / 2);
newForm.Show(dockPanel1, new Rectangle(topLeft, newForm.Size));

How to create a transparent control which works when on top of other controls?

I have a control (derived from System.Windows.Forms.Control) which needs to be transparent in some areas. I have implemented this by using SetStyle():
public TransparentControl()
{
SetStyle(ControlStyles.SupportsTransparentBackColor, true);
this.BackColor = Color.Transparent.
}
Now, this works if there are no controls between the form and the transparent control. However, if there happens to be another control below the transparent control (which is the use case here), it does not work. The intermediate control is not draw, but the form below does show through. I can get the effect that I need by overriding CreateParams and setting the WS_EX_TRANSPARENT flasg like so:
protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
cp.ExStyle |= 0x20; // WS_EX_TRANSPARENT
return cp;
}
}
The problem here is that it really slows down the painting of the control. The control is already double buffered, so nothing to do there. The performance hit is so bad that it is unacceptable. Has anyone else encountered this problem? All of the resources that I can find suggest using method #1, but again, that does not work in my case.
EDIT: I should note that I do have a workaround. The child (transparent) controls could simply draw themselves onto the parent's Graphics object, but it is really ungly and I don't like the solution at all (though it may be all I have).
EDIT2: So, following the advice that I got on how transparency works in .NET, I have implemented the IContainer interface in my user control which contains the transparent controls. I have created a class which implements ISite, I add my child controls to the Components collection of the UserControl, the Container property lines up correctly in the debugger, but I still do not get a transparency effect. Does anyone have any ideas?
This is just a simple thing I cooked up.. The only issue I've found is that it doesn't update when the intersecting controls are updated..
It works by drawing a control that's behind/intersects with the current control to a bitmap, then drawing that bitmap to the current control..
protected override void OnPaint(PaintEventArgs e)
{
if (Parent != null)
{
Bitmap behind = new Bitmap(Parent.Width, Parent.Height);
foreach (Control c in Parent.Controls)
if (c.Bounds.IntersectsWith(this.Bounds) & c != this)
c.DrawToBitmap(behind, c.Bounds);
e.Graphics.DrawImage(behind, -Left, -Top);
behind.Dispose();
}
}
Transparent controls in DotNet are implemented by having the transparent control's container draw itself in the transparent control's window and then having the transparent control draw itself. This process doesn't take into account the possibility of overlapping controls. So you will need to use some sort of work-around to make it work.
In some cases I've had success with complex nesting, but that's mostly only good for quick-and-dirty layering of bitmaps, and it doesn't solve any issues with partially overlapping controls.
I found out that the modifications below make things a bit faster:
if((this.BackColor == Color.Transparent) && (Parent != null)) {
Bitmap behind = new Bitmap(Parent.Width, Parent.Height);
foreach(Control c in Parent.Controls) {
if(c != this && c.Bounds.IntersectsWith(this.Bounds)) {
c.DrawToBitmap(behind, c.Bounds);
}
}
e.Graphics.DrawImage(behind, -Left, -Top);
behind.Dispose();
}
I also think that using this.Width / this.Height instead of Parent.Width / Parent.Height would be even faster, but I didn't have time to tinker with it.
Drawing the siblings under the control is possible, but it's ugly. The code below works reasonably well for me, it expands on the code given in the link in Ed S.' answer.
Possible pitfalls:
DrawToBitmap was introduced with .net 2.0, so don't expect it to work with anything older than that. But even then something like this may be possible by sending WM_PRINT to the sibling control; AFAIK that's what DrawToBitmap does internally.
It may also have problems if you have controls under your control that make use of WS_EX_TRANSPARENT since according to msdn that window style fiddles with the painting order. I haven't got any controls that use this style so I can't tell.
I'm running XP SP3 with VS2010, therefore this approach may have additional problems on Vista or W7.
Here's the code:
if (Parent != null)
{
float
tx = -Left,
ty = -Top;
// make adjustments to tx and ty here if your control
// has a non-client area, borders or similar
e.Graphics.TranslateTransform(tx, ty);
using (PaintEventArgs pea = new PaintEventArgs(e.Graphics,e.ClipRectangle))
{
InvokePaintBackground(Parent, pea);
InvokePaint(Parent, pea);
}
e.Graphics.TranslateTransform(-tx, -ty);
// loop through children of parent which are under ourselves
int start = Parent.Controls.GetChildIndex(this);
Rectangle rect = new Rectangle(Left, Top, Width, Height);
for (int i = Parent.Controls.Count - 1; i > start; i--)
{
Control c = Parent.Controls[i];
// skip ...
// ... invisible controls
// ... or controls that have zero width/height (Autosize Labels without content!)
// ... or controls that don't intersect with ourselves
if (!c.Visible || c.Width == 0 || c.Height == 0 || !rect.IntersectsWith(new Rectangle(c.Left, c.Top, c.Width, c.Height)))
continue;
using (Bitmap b = new Bitmap(c.Width, c.Height, e.Graphics))
{
c.DrawToBitmap(b, new Rectangle(0, 0, c.Width, c.Height));
tx = c.Left - Left;
ty = c.Top - Top;
// make adjustments to tx and ty here if your control
// has a non-client area, borders or similar
e.Graphics.TranslateTransform(tx, ty);
e.Graphics.DrawImageUnscaled(b, new Point(0, 0));
e.Graphics.TranslateTransform(-tx, -ty);
}
}
I decided to simply paint the parent under my child controls manually. Here is a good article.
Some suggestions (apologies for the VB code).
Try to avoid painting the background:
Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
If m.Msg = &H14 Then
Return
End If
MyBase.WndProc(m)
End Sub
Protected Overrides Sub OnPaintBackground(ByVal pevent As System.Windows.Forms.PaintEventArgs)
Return
End Sub
Don't call the controls base paint method:
Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
'MyBase.OnPaint(e) - comment out - do not call
End Sub
This does the trick, at least it has for me:
protected override void OnPaintBackground(PaintEventArgs e)
{
//base.OnPaintBackground(e);
this.CreateGraphics().DrawRectangle(new Pen(Color.Transparent, 1), new Rectangle(0, 0, this.Size.Width, this.Size.Height));
}

Categories