Measure ToolStripItem in custom ToolStrip renderer - winforms

I have a WinForms app for .NET Framework in which the text of items in context menus based on the the ContextMenuStrip component are drawn using Graphics.DrawString() to provide consistent look with other interface elements. The core part of the custom renderer looks like this:
private class CustomContextMenuRenderer : ToolStripProfessionalRenderer
{
private static StringFormat fStringFormatLeft = new StringFormat()
{ Alignment = StringAlignment.Near, LineAlignment = StringAlignment.Center};
private static StringFormat fStringFormatRight = new StringFormat()
{ Alignment = StringAlignment.Far, LineAlignment = StringAlignment.Center };
protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e)
{
StringFormat sf = (e.TextFormat & TextFormatFlags.Right) == 0 ? fStringFormatLeft : fStringFormatRight;
using (SolidBrush brush = new SolidBrush(e.Item.ForeColor))
{
e.Graphics.DrawString(e.Text, e.TextFont, brush, e.TextRectangle, sf);
}
}
}
For some fonts, the height of menu items calculated by the ContextMenuStrip component is not enough to display item text. Most likely, this happens because the standard implementation is based on the TextRenderer class to output item texts. Is there a way to measure and tell the component the expected size of the item if we use GDI+ to render item texts?

Related

Custom WPF canvas with multiple underlying visuals

Found interesting article describing how to create custom canvas control by exposing methods Add and Remove for underlying visuals of the Panel class. This way I could create universal canvas that can accommodate absolutely any way to draw on it, GDI+, Canvas, Drawings, etc. For example, the first layer would be Bitmap, second Canvas, third DrawingVisual, etc.
For simplicity, I'd like to extend existing Canvas, so I could have original behavior provided by default Canvas control + could create as many additional Visuals as I want to.
Here is what I have now.
public class VisualCanvas : Canvas
{
protected IList<Visual> _visuals = null;
protected override int VisualChildrenCount => _visuals.Count;
protected override Visual GetVisualChild(int index) => _visuals.ElementAtOrDefault(index);
public VisualCanvas()
{
_visuals = new List<Visual>();
_visuals.Add(new DrawingVisual());
//(_visuals[0] as DrawingVisual).RenderOpen();
}
public void AddVisual(Visual visual)
{
_visuals.Add(visual);
base.AddVisualChild(visual);
base.AddLogicalChild(visual);
}
public void DeleteVisual(Visual visual)
{
_visuals.Remove(visual);
base.RemoveVisualChild(visual);
base.RemoveLogicalChild(visual);
}
}
Unfortunately, DrawingVisual that I add in the constructor doesn't make this control to act like original Canvas because 2 methods that I overridden seem to expect different kind of Visual, not DrawingVisual.
How do I make this control work like original Canvas?
Found similar question on MSDN. Appears to be it's not enough to override only 2 methods to keep all children in one list of visuals. Default canvas collection InternalChildren also needs to be taken into account.
This implementation seems to keep default canvas behavior + new visuals, but may have unpredictable behavior because 2 class collections share the same index. Looks like it would be easier to extend this class with new properties for each visual type rather than trying to override existing ones. Feel free to post better ideas and implementations.
public class VisualCanvas : Canvas
{
protected IList<Visual> _visuals = new List<Visual>();
protected override int VisualChildrenCount => _visuals.Count + InternalChildren.Count;
protected override Visual GetVisualChild(int index) => _visuals.ElementAtOrDefault(index) ?? InternalChildren[index - _visuals.Count];
public void AddVisual(Visual visual)
{
_visuals.Add(visual);
base.AddVisualChild(visual);
base.AddLogicalChild(visual);
}
public void DeleteVisual(Visual visual)
{
_visuals.Remove(visual);
base.RemoveVisualChild(visual);
base.RemoveLogicalChild(visual);
}
}

Force ElementHost to redraw

We have a software that uses Windows Forms's built-in designer (IDesignerHost et al), and custom user controls you can place on the surface.
I'm moving portions of those controls to WPF, using ElementHost. When loading those controls and adding them programmatically, that works, fine. However, when the user interactively adds a new control, its ElementHost portion (in this example, the label at the top) is rendered entirely in black.
Until, that is, I move it around on the design surface.
So clearly, something can be done to force the ElementHost to draw its contents correctly. I'm just not entirely sure what.
There's a number of posts (e.g., ElementHost Layout Problems, ElementHost - Black Background when calling Show method, Black background before loading a wpf controll when using ElementHost), but all of them sound like hacks that don't solve the actual underlying problem.
(edit)
Here's the existing IDesignerHost.CreateComponent implementation:
<System.Diagnostics.DebuggerStepThrough()>
Public Function CreateComponent(componentClass As Type, name As String) As IComponent Implements IDesignerHost.CreateComponent
Dim Component As IComponent = Nothing
' Create instance
Component = CType(Activator.CreateInstance(componentClass), IComponent)
' Add to design container
Add(Component, name)
Return Component
End Function
Amending it with a Refresh() like so doesn't help:
[System.Diagnostics.DebuggerStepThrough]
public IComponent CreateComponent(Type componentClass, string name)
{
IComponent Component = null;
Component = (IComponent)Activator.CreateInstance(componentClass);
// Add to design container
Add(Component, name);
if (Component is UserControl newUC)
{
newUC.Refresh();
}
return Component;
}
Toggling Visible does seem to fix the issue:
[System.Diagnostics.DebuggerStepThrough]
public IComponent CreateComponent(Type componentClass, string name)
{
IComponent Component = null;
Component = (IComponent)Activator.CreateInstance(componentClass);
// Add to design container
Add(Component, name);
if (Component is UserControl newUC)
{
newUC.Visible = false;
newUC.Visible = true;
}
return Component;
}
But this seems quite hack-ish to me.

How to find WPF Page in navigation history to prevent opening the page again

How do I determine if a WPF page is still in the navigation history of WPF frame and then navigate back to it?
Summary:
My WPF application uses a single MainWindow containing two Frames one for menu layout and the other for display content. I am using MVVM pattern with content displayed as WPF pages as views with as little code as possible behind each view/page.
The content frame (shown in red) has a NavigationUI visible.
XAML:
Here is typical code to create WPF page and display it in the main window from a static helper class:
public static void ShowPeriodicTable()
{
var page = new Views.PeriodicTable();
(Application.Current.MainWindow as MainWindow).ContentArea.Navigate(page);
}
This code will load the same page over and over even if it is already loaded.
I've made the following change to detect if the current page displayed in the MainWindow.ContentArea (frame) before creating and navigating to the page.
public static void ShowPeriodicTable()
{
var currentPage = ((DRC_SQLITE.MainWindow)Application.Current.MainWindow).ContentArea.Content;
if (currentPage == null || (currentPage != null && currentPage.GetType().Name != "PeriodicTable"))
{
var page = new Views.PeriodicTable();
(Application.Current.MainWindow as MainWindow).ContentArea.Navigate(page);
}
}
Question:
How do detect if the page that is about to be opened exists in the navigation history but is not the current page in the frame. In the image below, the settingPage was opened twice. This is the scenario I want to eliminate to better manage memory usage of the application.
Pages are rarely used in commercial apps. As I understand it, they were really intended for xbap ( wpf in a browser ).
It's much more usual to have usercontrols hosted in a contentcontrol.
In which case there are two commonly used alternatives
1)
View First.
Hold a dictionary of usercontrols keyed by type.
https://gallery.technet.microsoft.com/WPF-Navigation-Basic-Sample-11f10c74
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Navigate(typeof(HomeView));
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Button btn = (Button)e.OriginalSource;
Navigate(btn.Tag as Type);
}
private void Navigate (Type viewType)
{
UserControl uc;
if (Views.ContainsKey(viewType))
{
uc = Views[viewType];
}
else
{
uc = (UserControl)Activator.CreateInstance(viewType);
Views[viewType] = uc;
}
NavigationParent.Content = uc;
}
private Dictionary<Type, UserControl> Views = new Dictionary<Type, UserControl>();
}
Proponents of this point out that navigation is arguably a view responsibility.
2)
Viewmodel first.
Bind the content of a contentcontrol to a property in your mainwindowviewmodel.
Switch this out to a viewmodel per view and template that into a usercontrol.
You then control retaining state by retaining a reference to a viewmodel for each view.
If you really want to stick with what you have then Frame.BackStack is the collection of entries in the journal. You could iterate that and check type of each object. I think it's actually a reference to a page. I have never seen this approach used in a commercial app.

How to recognize WPF drawing visuals in UIAutomation?

Our application has a canvas, to which we add the drawing visuals (like lines, polygons etc)
// sample code
var canvas = new Canvas(); // create canvas
var visuals = new VisualCollection(canvas); // link the canvas to the visual collection
visuals.Add(new DrawingVisual()); // add the visuals to the canvas
visuals.Add(new DrawingVisual());
Our goal is to add these visuals to the canvas via automation and validate that they are properly added. We use a framework that is based on Microsoft's UIAutomation.
When using a tool like "Inspect" to inspect the visual structure, I couldnt locate the canvas. Did some research and figured out that you need to override the OnCreateAutomationPeer method from UIElement, and return applicable AutomationPeer object to be able to be able to see that in automation.
Made the change and now I can see the canvas, however I cant still see any of the visuals added under the canvas.
Can anyone help me understand what the issue is?
Things tried / alternatives:
Tried to employ the OnCreateAutomationPeer technique, but the
DrawingVisuals dont derive from UIElement, and I cant add UIElements
to Canvas.VisualCollection.
Image recognition is an option, but we
are trying to avoid it for performance/maintenance considerations.
Only UIElement can be seen from UI Automation (like you have seen, OnCreateAutomationPeer starts from this class, not from the Visual class).
So you need to add UIElement (or derived like FrameworkElement) to the canvas, if you want it to be usable by UIAutomation.
You can create your own class like described here: Using DrawingVisual Objects or with a custom UserControl or use an existing one that suits your need but it must derive from UIElement somehow.
Once you have a good class, you can use the default AutomationPeer or override the method and adapt more closely.
If you want to keep Visual objects, one solution is to modify the containing object (but it still needs to derive from UIElement). For example, here if I follow the article in the link, I can write a custom containing object (instead of a canvas of your sample code so you may have to adapt slightly) like this:
public class MyVisualHost : UIElement
{
public MyVisualHost()
{
Children = new VisualCollection(this);
}
public VisualCollection Children { get; private set; }
public void AddChild(Visual visual)
{
Children.Add(visual);
}
protected override int VisualChildrenCount
{
get { return Children.Count; }
}
protected override Visual GetVisualChild(int index)
{
return Children[index];
}
protected override AutomationPeer OnCreateAutomationPeer()
{
return new MyVisualHostPeer(this);
}
// create a custom AutomationPeer for the container
private class MyVisualHostPeer : UIElementAutomationPeer
{
public MyVisualHostPeer(MyVisualHost owner)
: base(owner)
{
}
public new MyVisualHost Owner
{
get
{
return (MyVisualHost)base.Owner;
}
}
// a listening client (like UISpy is requesting a list of children)
protected override List<AutomationPeer> GetChildrenCore()
{
List<AutomationPeer> list = new List<AutomationPeer>();
foreach (Visual visual in Owner.Children)
{
list.Add(new MyVisualPeer(visual));
}
return list;
}
}
// create a custom AutomationPeer for the visuals
private class MyVisualPeer : AutomationPeer
{
public MyVisualPeer(Visual visual)
{
}
// here you'll need to implement the abstrat class the way you want
}
}

Draw large DrawingVisual in Windows Forms

Background
I have a large report in my WinForms application that is generated into a WPF DrawingVisual. I want to display the report in a separate resizable window that enables the users to scroll up/down as they read it. In this case the report doesn't have any page breaks and consist of one large page.
First attempt:
Use a customized DocumentViewer to display the report.
Added the DrawingVisual into a FixedDocument with a custom height, added the FixedDocument into a FixedDocumentSequence and finally passed the document sequence into the print preview window (that uses a custom DocumentViewer).
var previewWindow = new ReportPrintPreview(docSeq);
previewWindow.Show();
Classes:
public class ReportPrintPreview : Window
{
private readonly DocumentViewer docViewer;
public ReportPrintPreview(IDocumentPaginatorSource doc)
{
docViewer = new CustomDocumentViewer();
docViewer.Document = doc;
}
protected override void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
AddChild(docViewer);
}
}
public class CustomDocumentViewer : DocumentViewer
{
public CustomDocumentViewer()
{
ShowPageBorders = false;
}
protected override void OnPrintCommand()
{
}
}
This works and gives good performance for large reports, but I find the customization possibilities of the DocumentViewer limiting. Want to add combo boxes in the toolbar and control the zoom of the report when the window is resized for example.
Second attempt:
Use a regular Windows Forms form and draw the DrawingVisual using a ElementHost. The element host is placed inside a panel to enable scrolling.
var form = new ReportViewer(visual, pSize);
form.ShowDialog();
Classes:
public partial class ReportViewer : System.Windows.Forms.Form
{
public ReportViewer(DrawingVisual visual, Size size)
{
InitializeComponent();
var wpfPanel = new WpfDrawingUserControl(visual);
elementHost1.Width = size.Width;
elementHost1.Height = size.Height;
elementHost1.Child = wpfPanel;
}
}
public class WpfDrawingUserControl : System.Windows.Controls.UserControl
{
public WpfDrawingUserControl(DrawingVisual visual)
{
var image = new Image();
image.Source = new DrawingImage(visual.Drawing);
Content = image;
}
}
I create a UserControl that contains an image that is created from the DrawingGroup of the DrawingVisual.
This kind of works, but the performance when scrolling is bad. And it even crashes when the report is large enough.
Solution?
How can I do more effective drawing of the DrawingVisual in the form? I guess the whole DrawingVisual will be painted in the UserControl even when just a part of the report is showing in the scroll enabled panel.
Alternatively, how can I customize the DocumentViewer or create my own WPF control to display the DrawingVisual.

Resources