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.
Related
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?
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);
}
}
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
}
}
I made a project using WPF and I want to open its window in Revit I trying windows form it worked but wpf not opened !!
I use this
public virtual Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
IDesign.MainWindow testsd = new IDesign.MainWindow();
testsd.InitializeComponent();
//MessageBox.Show("notworking");
return Result.Succeeded;
}
But it didn't work any solutions
public virtual Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
IDesign.MainWindow testsd = new IDesign.MainWindow();
//testsd.InitializeComponent();
//MessageBox.Show("notworking");
return Result.Succeeded;
}
public MainWindow()
{
InitializeComponent();
this.ShowDialog();
}
You may want to create a WPF User Control and assign it as the Content of a WPF window in a Revit External Command, something like:
UserControl1 userControl = new UserControl1();
Window win = new Window();
win.Content = userControl;
win.Show();
I'm trying to create an overlay in wpf (with darkening background), similar to the ones you can find on the web to popup images.
I would like it to be reusable in more than 1 part of the application, with diffent types of content.
this is the temporary code of the constructor of the adorner class (just to try)
private readonly Grid _grid = new Grid();
public DarkOverlayAdorner(UIElement adornedElement, Object content) :
base(adornedElement)
{
_grid.Background = new SolidColorBrush(Color.FromArgb(99, 0, 0, 0));
IsHitTestVisible = true;
var visual = content as UIElement;
if (visual != null)
_grid.Children.Add(visual);
}
In addition in the class (of course), I have the ovverrides of MeasureOverride and ArrangeOverride to give the adorner the correct size of the adorned element, GetVisualChild, and VisualChildCount...
The problem here is that the adorner is correctly shown, but no events or behaviour are applied on the adorned element. For example:
AdornerLayer layer = AdornerLayer.GetAdornerLayer(textBoxProva);
layer.Add(new DarkOverlayAdorner(textBoxProva, new Button{Content = "prova"}));
The button here is shown, but I can-t click the button and no effects on button mouseover are applied.
I still can't figure out the problem.
Ok, I've lost a lot of time trying to figure out what was the problem.
In the end I found the solution:
If you want the element added to react to events, I think that the element must be bound to the visual tree of the adorner.
The way to do it is to use a VisualCollection, intitialized to the adorner itself:
VisualCollection visualChildren;
FrameworkElement #object;
public DarkOverlayAdorner(UIElement adornedElement) :
base(adornedElement)
{
visualChildren = new VisualCollection(this);
#object = new Button {Content = "prova"};
visualChildren.Add(#object);
}
protected override Visual GetVisualChild(int index)
{
return visualChildren[index];
}
This way the events are correctly routed.
You might want to take a look at the ChildWindow control in the Extended WPF Toolkit. It is a control that pops up a Window with a modal background effect, and you can specify the content to put inside the Window.