The code below shows a small WinForms app which includes a simple Control that draws a circle. I'm trying to understand the behavior of the Control.Scale method.
If I call the Scale method on the Control from Main, as shown in the code, it scales properly. But if I instead call Scale from Circle's constructor, no scaling occurs.
My puzzlement here no doubt indicates a gross misunderstanding on my part regarding what Scale is supposed to do. Can anyone enlighten me?
using System;
using System.Windows.Forms;
using System.Drawing;
class Program
{
[STAThread]
public static void Main()
{
var circle = new Circle(Color.Orange)
{
Size = new Size(23, 23),
Location = new Point(50, 50)
};
circle.Scale(new SizeF(3.0f, 3.0f)); // <-- scaling here works
var form = new Form();
form.Controls.Add(circle);
Application.Run(form);
}
}
class Circle : Control
{
public Circle(Color color)
{
ForeColor = color;
// Scale(new SizeF(3.0f, 3.0f)); // <-- scaling here DOESN'T work
}
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.FillEllipse(new SolidBrush(ForeColor), ClientRectangle);
}
}
The Scale() method isn't meant to do this. It is a helper method to implement the AutoScaleMode property. When your control is created by the form's InitializeComponent() method, scaling is suspended with SuspendLayout(). Which is why it has no effect in your constructor. The AutoScaleMode property value is applied when the form handle is created. Which cancels any scaling you applied.
I think you are looking for e.Graphics.ScaleTransform() in your OnPaint method. It doesn't scale the control, it scales the drawing. If you really did mean to scale the control then just change its Size property.
Related
I've looked for quite a while now for a way to be able to tell a WPF control (or window) to keep a certain aspect ratio.
For a Window I found this solution, that works quite well. But since it uses the Win32 API and window handles it's not working for any WPF Controls (because as far as I know in WPF only the window itself has a handle)
For a Control one usually gets the advice to put the Control in a ViewBox, but I don't want to scale my controls, I want them to resize (and keep any border width or font size).
Other "solutions" for a Control involve any form of binding the Width to the ActualHeight or the Height to the ActualWidth, or using the SizeChanged event, but this results in heavy flickering while resizing and it's not very reliable.
In case of binding the Width to the ActualHeight you can't resize only the Width (by dragging the right border) because the ActualHeight doesn't change.
In case of the event it gets tricky when width and height change at the same time, then you'd have to change the size inside the SizeChanged event... and did I mention the flickering?
After a lot of reading and searching I came to the conclusion that the best way to force any control to keep a certain aspect ratio would be to do that inside the Measure and Arrange functions.
I found this solution that creates a Decorator control with overridden Measure and Measure functions, but that would mean to put any control that's supposed to keep it's aspect ratio inside it's own Decorator. I could live with that if I had to, but I wonder if there's a better way to do it.
So, here's my question. Is it possible to create an attached property Ratio and an attached property KeepRatio and somehow override the Measure and Arrange functions of the controls in question in the OnKeepRatioChanged and RatioChanged callbacks of the attached properties?
If you want to override Arrange/Measure methods then there is no need in attached properties. This wrapper should be fine:
public partial class RatioKeeper : UserControl
{
public static readonly DependencyProperty VerticalAspectProperty = DependencyProperty.Register(
"VerticalAspect", typeof(double), typeof(RatioKeeper), new PropertyMetadata(1d));
public static readonly DependencyProperty HorizontalAspectProperty = DependencyProperty.Register(
"HorizontalAspect", typeof(double), typeof(RatioKeeper), new PropertyMetadata(1d));
public double HorizontalAspect
{
get { return (double) GetValue(HorizontalAspectProperty); }
set { SetValue(HorizontalAspectProperty, value); }
}
public double VerticalAspect
{
get { return (double) GetValue(VerticalAspectProperty); }
set { SetValue(VerticalAspectProperty, value); }
}
public RatioKeeper()
{
InitializeComponent();
}
//arrangeBounds provides size of a host.
protected override Size ArrangeOverride(Size arrangeBounds)
{
//Calculation of a content size that wont exceed host's size and will be of the desired ratio at the same time
var horizontalPart = arrangeBounds.Width / HorizontalAspect;
var verticalPart = arrangeBounds.Height / VerticalAspect;
var minPart = Math.Min(horizontalPart, verticalPart);
var size = new Size(minPart * HorizontalAspect, minPart * VerticalAspect);
//apply size to wrapped content
base.ArrangeOverride(size);
//return size to host
return size;
}
}
I created an application using Microsoft ribbon for WPF. I used RibbonWindow instead of simple Window to place QuickAccessToolbar to window header.
The problem is that normal Window becomes fullscreen when i set
WindowStyle="None"
WindowState="Maximized"
But RibbonWindow becomes bigger and its bottom part hides behind taskbar.
I suppose that the only difference between window and RibbonWindows is the controlTemplate.
But i dont actually understand how can i via template place the window above the taskbar.
Any ideas how to show my RibbonWindow above taskbar just as normal window does?
Link to the VS2010 project (10KiB) (Microsoft Ribbon For WPF isn't included)
RibbonWindow uses custom WindowChrome. That is the reason of incorrect behavior. Try this code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using Microsoft.Windows.Shell;
namespace Utils
{
public static class WindowHelper
{
public static void Fullscreen(Window p_oWindow)
{
p_oWindow.WindowStyle = WindowStyle.None;
p_oWindow.Topmost = true;
p_oWindow.WindowState = WindowState.Maximized;
WindowChrome.SetWindowChrome(p_oWindow, null);
}
public static void UndoFullscreen(Window p_oWindow)
{
p_oWindow.WindowStyle = WindowStyle.SingleBorderWindow;
p_oWindow.Topmost = false;
p_oWindow.WindowState = WindowState.Normal;
WindowChrome.SetWindowChrome(p_oWindow, GetChrome());
}
public static WindowChrome GetChrome()
{
WindowChrome oCustomChrome = new WindowChrome();
oCustomChrome.CornerRadius = new System.Windows.CornerRadius(0, 0, 0, 0);
oCustomChrome.CaptionHeight = 0;
oCustomChrome.ResizeBorderThickness = new System.Windows.Thickness(2, 2, 2, 2);
return oCustomChrome;
}
}
}
The problem is the WindowStyle. This happens in all WPF windows when you remove the chrome. (They really optimized this feature for full-screen kiosk apps).
In order to handle maximizing your window to the correct size, you're going to need to handle it yourself. When you maximize button is clicked, you will need to get the working area of the current monitor you're on. There isn't a WPF way of doing this, but you can use the WinForms Screen class.
Your maximize method would look like so:
// you must save the restore bounds, since we never set Window.WindowState,
// so the RestoreBounds property will get set to the maximized bounds
private System.Drawing.Rectangle restoreBounds;
private void DoMaximize(object sender, EventArgs e)
{
var bounds = new System.Drawing.Rectangle((int)Left, (int)Top,
(int)ActualWidth, (int)ActualHeight));
var screen = System.Windows.Forms.Screen.FromRectangle(bounds);
var area = screen.WorkingArea;
restoreBounds = bounds;
Left = area.X;
Top = area.Y;
Width = area.Width;
Height = area.Height;
}
I created an adorner on a WPF line element, because there was neet to add some text.
Now, when this line is moved, the adorner does not "follow" the line automatically. In fact, it does not refresh itsef:
here black curves is the Control drawing, and the red "120 m" is the adorner one.
Some code
void SegmentLine_Loaded(object sender, RoutedEventArgs e)
{
AdornerLayer aLayer = AdornerLayer.GetAdornerLayer(this);
if (aLayer != null)
{
aLayer.Add(new TextAdorner(this));
}
}
class TextAdorner : Adorner
{
public TextAdorner(UIElement adornedElement)
: base(adornedElement)
{
}
protected override void OnRender(DrawingContext drawingContext)
{
SegmentLine segment = (this.AdornedElement as SegmentLine);
if (segment != null)
{
Rect segmentBounds = new Rect(segment.DesiredSize);
var midPoint = new Point(
(segment.X1 + segment.X2) / 2.0,
(segment.Y1 + segment.Y2) / 2.0);
var lineFont = // get line font as Font
FormattedText ft = new FormattedText(
string.Format("{0} m", segment.Distance),
Thread.CurrentThread.CurrentCulture,
System.Windows.FlowDirection.LeftToRight,
new Typeface(lineFont.FontFamily.ToString()),
ligneFont.Size, Brushes.Red);
drawingContext.DrawText(ft, midPoint);
}
}
}
Why MeasureOverride, etc aren't being called
Your adorner's MeasureOverride, ArrangeOverride, and OnRender aren't being called because your SegmentLine control is never changing size or position:
Since your SegmentLine doesn't implement MeasureOverride, it always has the default size assigned by the layout engine.
Since your SegmentLine doesn't implement ArrangeOverride or manipulate any transforms, its position is always exactly the upper-left corner of the container.
The Adorner's MeasureOverride, ArrangeOverride and OnRender are only called by WPF under these conditions:
The AdornedElement changes size or position (this the most common case), or
One of the Adorner's properties chagnes and that property is marked AffectsMeasure, AffectsArrange, or AffectsRender, or
You call InvalidateMeasure(), InvalidateArrange(), or InvalidateVisuaul() on the adorner.
Because your SegmentLine never changes size or position, case 1 doesn't apply. Since you don't have any such properties on the Adorner and don't call InvalidateMeasure(), InvalidateArrange() or InvalidateVisual(), the other cases don't apply either.
Precise rules for Adorner re-measure
Here are the precise rules for when an adorned element change triggers a call to Adorner.MeasureOverride:
The adorned element must force a layout pass by invalidating its Measure or Arrange in response to some event. This could be triggered automatically by a change to a DependencyProperty with AffectsMeasure or AffectsArrange, or by a direct call to InvalidateMeasure(), InvalidateArrange() or InvalidateVisual().
The adorned element's Measure and Arrange methods must not be called directly from user code between the invalidation and the layout pass. In other words, you must wait for the layout manager to do the job.
The adorned element must make a non-trivial change to either its RenderSize or its Transform.
The combination of all transforms between the AdornerLayer and the adorned element must be affine. This will generally be the case as long as you are not using 3D.
Your SegmentLine is just drawing the line in a new place rather than updating its own dimensions, thereby omitting my requirement #3 above.
Recommendation
Normally I would recommend your adorner have AffectsRender DependencyProperties bound to the SegmentLine's properties, so any time X1, Y1, etc change in the SegmentLine they are also updated in the Adorner which causes the Adorner to re-render. This provides a very clean interface, since the adorner can be used on any control that has properties X1, Y1, etc, but it is less efficient than tightly coupling them.
In your case the adorner is clearly tightly bound to your SegmentLine, so I think it makes just as much sense to call InvalidateVisual() on the adorner from the SegmentLine's OnRender(), like this:
public class SegmentLine : Shape
{
TextAdorner adorner;
...
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
if(adorner==null)
{
var layer = AdornerLayer.GetAdornerLayer(this); if(layer==null) return;
adorner = new TextAdorner(this);
... set other adorner properties and events ...
layer.Add(adorner);
}
adorner.InvalidateVisual();
}
}
Note that this doesn't deal with the situation where the SegmentLine is removed from the visual tree and then added again later. Your original code doesn't deal with this either, so I avoided the complexity of dealing with that case. If you need that to work, do this instead:
public class SegmentLine : Shape
{
AdornerLayer lastLayer;
TextAdorner adorner;
...
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
var layer = AdornerLayer.GetAdornerLayer(this);
if(layer!=lastLayer)
{
if(adorner==null)
{
adorner = new TextAdorner(this);
... set other adorner properties and events ...
}
if(lastLayer!=null) lastLayer.Remove(adorner);
if(layer!=null) layer.Add(adorner);
lastLayer = layer;
}
adorner.InvalidateVisual();
}
}
How is the line being moved? Does the MeasureOverride or ArrangeOverride of the adorner get invoked after the move? OnRender will only get invoked if the visual is invalidated (e.g. invalidatevisual) so I'm guessing that the render isn't being invalidated.
May be you wanted to use segmentBounds to define midPoint? Otherwise what is it doing there? Looks like you are defining midPoint relative to not rerendered segment.
idiot fix, but it works
AdornerLayer aLayer;
void SegmentLine_Loaded(object sender, RoutedEventArgs e)
{
aLayer = AdornerLayer.GetAdornerLayer(this);
if (aLayer != null)
{
aLayer.Add(new TextAdorner(this));
}
}
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
if (aLayer != null)
{
aLayer.Update();
}
}
Now, the problem is that when I click on a the adorner the control itself does not recieve the hit...
I restore coordinates of Window on application startup. In good-old-Windows-Forms I used System.Windows.Forms.Screen collection. Is there anything similar in WPF world?
I did notice PrimaryScreen*, VirtualScreen* parameters in System.Windows.SystemParameters. However they left me hanging since it seems to be impossible to detect whether Window is inside bounds in cases when monitors are not same size.
System.Windows.Forms.Screen works perfectly well within WPF, so I think the designers of WPF saw no advantage in replacing it with a WPF-specific version.
You'll have to do a coordinate transformation of course. Here's an easy class to do the conversion:
public class ScreenBoundsConverter
{
private Matrix _transform;
public ScreenBoundsConverter(Visual visual)
{
_transform =
PresentationSource.FromVisual(visual).CompositionTarget.TransformFromDevice;
}
public Rect ConvertBounds(Rectangle bounds)
{
var result = new Rect(bounds.X, bounds.Y, bounds.Width, bounds.Height);
result.Transform(_transform);
return result;
}
}
Example usage:
var converter = new ScreenBoundsConverter(this);
foreach(var screen in System.Windows.Forms.Screen.AllScreens)
{
Rect bounds = converter.ConvertBounds(screen.Bounds);
...
}
I'm using WPF in WinForms with ElementHost. When the form loads, there is a flash of black background where the ElementHost is about to load. This looks kind of bad. Any suggestions on how to get rid of this?
Hide the element (Visibility = Hidden) until the WinForms control is fully loaded...
I know this has already been answered and the question is old but none of the presented answers worked for myself and after a long time of troubleshooting the issue. I finally found an easier answer.
If you build a class extending from Element Host and in the initial constructor. You can set a Load Event for the Host Container. The Host Container is the panel that the Element Hosts Child is being displayed on top of. From there, just set the Host Containers background color to being of the Element Hosts Parents background color.
Like this
using System.Windows;
using System.Windows.Forms;
using System.Windows.Media;
public class MyElementHost : ElementHost
{
public MyElementHost()
{
this.HostContainer.Loaded += new RoutedEventHandler(HostPanelLoad);
}
public void HostPanelLoad(object sender, RoutedEventArgs e)
{
System.Drawing.Color parentColor = this.Parent.BackColor;
this.HostContainer.Background = new SolidColorBrush(Color.FromArgb(parentColor.A, parentColor.R, parentColor.G, parentColor.B));
}
}
you need first show control with empty bounds first time to avoid black flickering
if (!_control.Created && _control.BackColor != Color.Transparent)
{
_control.Bounds = Rectangle.Empty;
_control.Show();
}
// set control bounds and show it
Rectangle bounds = GetBounds(context, rect);
if (_control.Bounds != bounds)
_control.Bounds = bounds;
if (!_control.Visible)
_control.Show();