Displaying about a Meg of Text in WPF - wpf

I have a barebones WPF app that has about a Meg of ASCII text to display. I initially put a TextBlock in a WrapPanel in a ScrollViewer. This correctly scrolled and resized when I resized the window, but it was super slow! I needed something faster.
So I put the text in FormattedText, and rendered that using a custom control. That was much faster, but it didn't resize. So I made my custom control resize. But it would ReDraw too many times a second, so I made it only redraw every 100ms.
Much better. Rendering and Resizing still isn't great but it's much better than it was. But I lost scrolling.
Eventually I need a solution that does a lot - but for now I'm trying to have a solution that does a little: show a mem of text, wrap, have a scrollbar, and be performant. Eventually, I'd like it to scale to a gig of text, have colors inline, some mouseover/click events for portions of the text...
How can I make FormattedText (or perhaps more accurately, a DrawingVisual) have a Vertical Scrollbar?
Here's my FrameworkElement that shows my FormattedText:
using System;
using System.Windows;
using System.Windows.Media;
namespace Recall
{
public class LightweightTextBox : FrameworkElement
{
private VisualCollection _children;
private FormattedText _formattedText;
private System.Threading.Timer _resizeTimer;
private const int _resizeDelay = 100;
public double MaxTextWidth
{
get { return this._formattedText.MaxTextWidth; }
set { this._formattedText.MaxTextWidth = value; }
}
public LightweightTextBox(FormattedText formattedText)
{
this._children = new VisualCollection(this);
this._formattedText = formattedText;
DrawingVisual drawingVisual = new DrawingVisual();
DrawingContext drawingContext = drawingVisual.RenderOpen();
drawingContext.DrawText(this._formattedText, new Point(0, 0));
drawingContext.Close();
_children.Add(drawingVisual);
this.SizeChanged += new SizeChangedEventHandler(LightweightTextBox_SizeChanged);
}
void LightweightTextBox_SizeChanged(object sender, SizeChangedEventArgs e)
{
this.MaxTextWidth = e.NewSize.Width;
if (_resizeTimer != null)
_resizeTimer.Change(_resizeDelay, System.Threading.Timeout.Infinite);
else
_resizeTimer = new System.Threading.Timer(new System.Threading.TimerCallback(delegate(object state)
{
ReDraw();
if (_resizeTimer == null) return;
_resizeTimer.Dispose();
_resizeTimer = null;
}), null, _resizeDelay, System.Threading.Timeout.Infinite);
}
public void ReDraw()
{
this.Dispatcher.Invoke((Action)(() =>
{
var dv = _children[0] as DrawingVisual;
DrawingContext drawingContext = dv.RenderOpen();
drawingContext.DrawText(this._formattedText, new Point(0, 0));
drawingContext.Close();
}));
}
//===========================================================
//Overrides
protected override int VisualChildrenCount { get { return _children.Count; } }
protected override Visual GetVisualChild(int index)
{
if (index < 0 || index >= _children.Count)
throw new ArgumentOutOfRangeException();
return _children[index];
}
}
}

For simple text a readonly TextBox is pretty good. For more complex matters you can use FlowDocuments (which can be hosted in a FlowDocumentScrollViewer), TextBlocks also host flow content but are not intended for larger amounts.
MSDN:
TextBlock is not optimized for scenarios that need to display more than a few lines of content; for such scenarios, a FlowDocument coupled with an appropriate viewing control is a better choice than TextBlock, in terms of performance. After TextBlock, FlowDocumentScrollViewer is the next lightest-weight control for displaying flow content, and simply provides a scrolling content area with minimal UI. FlowDocumentPageViewer is optimized around "page-at-a-time" viewing mode for flow content. Finally, FlowDocumentReader supports the richest set functionality for viewing flow content, but is correspondingly heavier-weight.

Related

TreeView: Place selection indicator along the item at the left edge of the control

So the requirement is simple, but the solution doesn't seem to be (or at least I haven't succeeded yet). I need to display a vertical bar at the left side of the currently selected item of the TreeView control. Something like this:
Problem I'm facing is that with child items, this indicator also moves towards right, as it is part of the ItemTemplate, like this:
This is undesirable. I need the red indicator to stick to the left edge of the control, like this:
I can see why this happens. The ItemsPresenter in TreeViewItem template introduces a left margin of 16 units, which causes the all child items to move right-wards as well. I can't figure out how to avoid it.
Note: The red bar is a Border with StrokeThickness set to 4,0,0,0. It encompasses the Image and TextBlock elements inside it, though this doesn't directly have anything to do with the problem.
As you are aware, since the left vacant space is outside of ItemsPresenter which hosts the content of TreeViewItem, you cannot accomplish it by ordinary Style.
Instead, a workaround would be to change the bar to an element such as Rentangle and move it to the edge of TreeView. For example, it can be done by an attached property which is to be attached to the element and move it to the edge of TreeView with a specified left margin.
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
public static class TreeViewHelper
{
public static double? GetLeftMargin(DependencyObject obj)
{
return (double?)obj.GetValue(LeftMarginProperty);
}
public static void SetLeftMargin(DependencyObject obj, double value)
{
obj.SetValue(LeftMarginProperty, value);
}
public static readonly DependencyProperty LeftMarginProperty =
DependencyProperty.RegisterAttached("LeftMargin", typeof(double?), typeof(TreeViewHelper), new PropertyMetadata(null, OnValueChanged));
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if ((d is FrameworkElement element) && (e.NewValue is double leftMargin))
{
element.Loaded += (_, _) =>
{
TreeView? tv = GetTreeView(element);
if (tv is null)
return;
Point relativePosition = element.TransformToAncestor(tv).Transform(new Point(0, 0));
element.RenderTransform = new TranslateTransform(leftMargin - relativePosition.X, 0);
};
}
}
private static TreeView? GetTreeView(FrameworkElement element)
{
DependencyObject test = element;
while (test is not null)
{
test = VisualTreeHelper.GetParent(test);
if (test is TreeView tv)
return tv;
}
return null;
}
}
Edit:
This workaround does not depend on how to show/hide the bar upon selection of the ListViewItem. Although the question does not provide the actual code for this, if you implement a mechanism to change BorderBrush upon selelection, you can modify it to change Fill of the bar (in the case of Rectangle).

How to programatically resize a DrawingVisual?

So, I'm new to WPF Drawing. For performance reasons, I've had to switch from regular controls like ContentControl and UserControl to more light-weight elements like DrawingVisual. I am working on a diagramming app which would probably have a max of 1000 elements on the canvas that can be dragged, resized and such. Firstly, is it better to use DrawingVisual instead of Shape?
Secondly, my main question here. I am adding DrawingVisual elements to the Canvas as such:
public class SVisualContainer : UIElement
{
// Create a collection of child visual objects.
private VisualCollection _children;
public SVisualContainer()
{
_children = new VisualCollection(this);
_children.Add(CreateDrawingVisualRectangle());
}
// Create a DrawingVisual that contains a rectangle.
private DrawingVisual CreateDrawingVisualRectangle()
{
DrawingVisual drawingVisual = new DrawingVisual();
// Retrieve the DrawingContext in order to create new drawing content.
DrawingContext drawingContext = drawingVisual.RenderOpen();
// Create a rectangle and draw it in the DrawingContext.
Rect rect = new Rect(new System.Windows.Point(160, 100), new System.Windows.Size(320, 80));
drawingContext.DrawRectangle(System.Windows.Media.Brushes.LightBlue, null, rect);
// Persist the drawing content.
drawingContext.Close();
return drawingVisual;
}
// Provide a required override for the VisualChildrenCount property.
protected override int VisualChildrenCount
{
get { return _children.Count; }
}
// Provide a required override for the GetVisualChild method.
protected override Visual GetVisualChild(int index)
{
if (index < 0 || index >= _children.Count)
{
throw new ArgumentOutOfRangeException();
}
return _children[index];
}
}
And within the canvas:
public void AddStateVisual()
{
var sVisual = new SVisualContainer();
Children.Add(sVisual);
Canvas.SetLeft(sVisual, 10);
Canvas.SetTop(sVisual, 10);
}
How can I increase the size of the Rectangle dynamically through code? I have tried setting the Height and Width of the Rectangle which did not work, played around with the ScaleTransform but that is probably not what I want. Would I need to redraw the Rectangle? Thanks!
I ended up using DrawingVisual within UIElement as shown in the question, and continuously redrawing the DrawingVisual upon resize. The UIElement.RenderSize property, UIElement.MeasureCore method and UIElement.InvalidateMeasure method are central to this. This works quite well and the performance is acceptable.

How to force vertical scrollbar always be visible from AutoScroll in WinForms?

Using VS2010 and .NET 4.0 with C# and WinForms:
I always want a Vertical Scrollbar to show for my panel as a disabled scrollbar (when it's not needed, and a enabled one when it can be used.
So it's like a hybrid AutoScroll. I've tried using VScrollBars but I can't figure out where to place them to make this work.
Essentially I've got a user control that acts as a "Document" of controls, its size changes so when using auto-scroll it works perfectly. The scrollbar appears when the usercontrol doesn't fit and the user can move it updown.
It's like a web browser essentially. However, redrawing controls takes a long time (it's forms with many fields and buttons etc within groups in a grid within a panel :P
So anyhow, when autoscroll enables the vertical scrollbar, it takes a while to redraw the window. I'd like to ALWAYS show the vertical scrollbar as indicated above (with the enable/disable functionality).
If anyone has some help, i've read many posts on the subject of autoscroll, but noone has asked what I'm asking and I can't come up with a solution.
C# Version of competent_Tech's answer
using System.Runtime.InteropServices;
public class MyUserControl : UserControl
{
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool ShowScrollBar(IntPtr hWnd, int wBar, bool bShow);
private enum ScrollBarDirection
{
SB_HORZ = 0,
SB_VERT = 1,
SB_CTL = 2,
SB_BOTH = 3
}
public MyUserControl()
{
InitializeComponent();
ShowScrollBar(this.Handle, (int) ScrollBarDirection.SB_VERT, true);
}
}
You can use the auto-scroll functionality of the panel, you just need to send it a windows message to show the vertical scrollbar:
<DllImport("user32.dll")> _
Public Shared Function ShowScrollBar(ByVal hWnd As System.IntPtr, ByVal wBar As Integer, ByVal bShow As Boolean) As Boolean
End Function
Private Const SB_VERT As Integer = 1
Public Sub New()
' This call is required by the designer.
InitializeComponent()
ShowScrollBar(Panel1.Handle, SB_VERT, True)
End Sub
The scrollbar will be displayed and appear as though it can be scrolled, but it won't do anything until it is actually ready to scroll. If you disable it, it won't be automatically re-enabled, so this is probably the best approach.
Also, to improve the performance while resizing, you can call SuspendLayout on the panel before updating and ResumeLayout when done.
What worked for me was overriding the CreateParams call and enabling the WS_VSCROLL style.
public class VerticalFlowPanel : FlowLayoutPanel
{
protected override CreateParams CreateParams
{
get
{
var cp = base.CreateParams;
cp.Style |= 0x00200000; // WS_VSCROLL
return cp;
}
}
}
The AutoScroll logic will now adjust the scrolling bounds without ever hiding the scrollbar.
Here is what solved this for me. My case is that I have a panel sandwiched between another three panels with no degree of liberty in any direction. I needed this panel to be so big that the whole structure would go out of my 1920x1080 screen.
The solution is actually very simple.
For the panel that needs scroll bars set the AutoScroll property to true. Then, add on it another control in the far right far down position (right-bottom position). The control I choose is a label which I made invisible.... And that is all.
Now my panel occupies its restricted area, but I can scroll to the size that I needed and use it for the size I need.
If you only need horizontal scroll bars add the invisible control outside left, for vertical only far down bottom.
The actual size of the panel is the one you restrict it to when display it, but the virtual size is dictated by the invisible control.
This code will draw a disabled vertical scrollbar whenever the built in scrollbar of the Panel is invisible. The codes assumes that
AutoScroll = true;
AutoSize = false;
The following code is speed-optimized. It does as few as possible in OnPaint().
Derive a class from Panel.
Add these member variables:
// NOTE: static variables are not thread safe.
// But as we have only one GUI thread this does not matter.
static IntPtr mh_ScrollTheme = IntPtr.Zero;
static int ms32_ScrollWidth = SystemInformation.VerticalScrollBarWidth;
Win32.RECT mk_ScrollTop;
Win32.RECT mk_ScrollBot; // coordinates of top scrollbar button
Win32.RECT mk_ScrollShaft; // coordinates of bottom scrollbar button
Then override OnSizeChanged:
protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
Win32.RECT k_ScrollBar = new Win32.RECT(ClientRectangle);
k_ScrollBar.Left = k_ScrollBar.Right - ms32_ScrollWidth;
mk_ScrollTop = new Win32.RECT(k_ScrollBar);
mk_ScrollBot = new Win32.RECT(k_ScrollBar);
mk_ScrollShaft = new Win32.RECT(k_ScrollBar);
int s32_Upper = k_ScrollBar.Top + ms32_ScrollWidth;
int s32_Lower = k_ScrollBar.Bottom - ms32_ScrollWidth;
mk_ScrollTop .Bottom = s32_Upper;
mk_ScrollBot .Top = s32_Lower;
mk_ScrollShaft.Top = s32_Upper;
mk_ScrollShaft.Bottom = s32_Lower;
}
And paint the scrollbar when required:
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
if (VScroll)
return; // The 'real' scrollbar is visible
if (mh_ScrollTheme == IntPtr.Zero)
mh_ScrollTheme = Win32.OpenThemeData(Handle, "SCROLLBAR");
if (mh_ScrollTheme == IntPtr.Zero)
return; // The user has disabled themes
// Draw the disabled vertical scrollbar.
IntPtr h_DC = e.Graphics.GetHdc();
// Draw shaft
const int SBP_UPPERTRACKVERT = 7;
const int SCRBS_DISABLED = 4;
Win32.DrawThemeBackground(mh_ScrollTheme, h_DC, SBP_UPPERTRACKVERT, SCRBS_DISABLED, ref mk_ScrollShaft, IntPtr.Zero);
// Draw top button
const int SBP_ARROWBTN = 1;
const int ABS_UPDISABLED = 4;
Win32.DrawThemeBackground(mh_ScrollTheme, h_DC, SBP_ARROWBTN, ABS_UPDISABLED, ref mk_ScrollTop, IntPtr.Zero);
// Draw lower button
const int ABS_DOWNDISABLED = 8;
Win32.DrawThemeBackground(mh_ScrollTheme, h_DC, SBP_ARROWBTN, ABS_DOWNDISABLED, ref mk_ScrollBot, IntPtr.Zero);
e.Graphics.ReleaseHdc(h_DC);
}
For some years, the answer of BradJ and fiat worked for me. Now I needed to show the disabled scrollbar look. But I failed to find the correct way… So here is my workaround.
The code bellow just draw the disabled scrollbar at the position of the real scrollbar.
THE CODE
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Windows.Forms.VisualStyles;
public class VerticalFlowPanel : FlowLayoutPanel
{
public VerticalFlowPanel()
{
AutoScroll = true;
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
var width = Width;
var height = Height;
var vsWidth = SystemInformation.VerticalScrollBarWidth;
var vsHeight = SystemInformation.VerticalScrollBarArrowHeight;
var left = width - vsWidth;
var sbUpper = new Rectangle(left, 0, vsWidth, height / 2);
var sbLower = new Rectangle(left, sbUpper.Height, vsWidth, height - sbUpper.Height);
var arUp = new Rectangle(left, 0, vsWidth, vsHeight);
var arDown = new Rectangle(left, height - vsHeight, vsWidth, vsHeight);
ScrollBarRenderer.DrawUpperVerticalTrack(e.Graphics, sbUpper, ScrollBarState.Disabled);
ScrollBarRenderer.DrawLowerVerticalTrack(e.Graphics, sbLower, ScrollBarState.Disabled);
ScrollBarRenderer.DrawArrowButton(e.Graphics, arUp, ScrollBarArrowButtonState.UpDisabled);
ScrollBarRenderer.DrawArrowButton(e.Graphics, arDown, ScrollBarArrowButtonState.DownDisabled);
}
// Necessary to avoid visual artifacts
protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
var width = Width;
var height = Height;
var vsWidth = SystemInformation.VerticalScrollBarWidth;
var scrollBounds = new Rectangle(width - vsWidth, 0, vsWidth, height);
Invalidate(scrollBounds);
}
}
NOTE
This is not the best solution. But it was easier than migrate my hole solution to WPF…

Building custom TextBlock control in WPF

I have built custom WPF Control which unique function is displaying text. I tried using TextBlock from System.Windows.Controls namespace but it's not working for me (I have ~10000 strings with different position and too much memory loss). So I tried making my own control by inheriting FrameworkElement, overriding OnRender method which now contain single line:
drawingContext.DrawText(...);
But...
I get a little confusing result.
After comparing performance for 10000 objects, I realized that the time needed for creating and adding to Canvas is still ~10 sec, and memory usage for my application raises from ~32MB to ~60MB !!!
So no benefits at all.
Can anyone explain why this happens, and what is the other way to create simple (simple = allocate less memory, take less time to create) visual with two functions:
display text
set position (using thickness or TranslateTransform)
Thanks.
Check out AvalonEdit
Also not sure how you are storing the strings, but have you used StringBuilder before?
Here is my code (a little bit modified):
public class SimpleTextBlock : FrameworkElement
{
#region Static
private const double _fontSize = 12;
private static Point _emptyPoint;
private static Typeface _typeface;
private static LinearGradientBrush _textBrush;
public readonly static DependencyProperty TextWidthProperty;
static SimpleTextBlock()
{
_emptyPoint = new Point();
_typeface = new Typeface(new FontFamily("Sergoe UI"), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);
GradientStopCollection GSC = new GradientStopCollection(2);
GSC.Add(new GradientStop(Color.FromArgb(160, 255, 255, 255), 0.0));
GSC.Add(new GradientStop(Color.FromArgb(160, 180, 200, 255), 0.7));
_textBrush = new LinearGradientBrush(GSC, 90);
_textBrush.Freeze();
SimpleTextBlock.TextWidthProperty = DependencyProperty.Register(
"TextWidth",
typeof(double),
typeof(SimpleTextBlock),
new FrameworkPropertyMetadata(0.0d, FrameworkPropertyMetadataOptions.AffectsRender));
}
#endregion
FormattedText _formattedText;
public SimpleTextBlock(string text)
{
_formattedText = new FormattedText(text, System.Globalization.CultureInfo.InvariantCulture, FlowDirection.LeftToRight, _typeface, _fontSize, _textBrush);
}
public SimpleTextBlock(string text, FlowDirection FlowDirection)
{
_formattedText = new FormattedText(text, System.Globalization.CultureInfo.InvariantCulture, FlowDirection, _typeface, _fontSize, _textBrush);
}
protected override void OnRender(DrawingContext drawingContext)
{
_formattedText.MaxTextWidth = (double)GetValue(TextWidthProperty);
drawingContext.DrawText(_formattedText, _emptyPoint);
}
public double TextWidth
{
get { return (double)base.GetValue(TextWidthProperty); }
set { base.SetValue(TextWidthProperty, value); }
}
public double ActualTextWidth
{
get { return _formattedText.Width; }
}
public double ActualTextHeight
{
get { return _formattedText.Height; }
}
}
Since it sounds like we determined you should stylize a control like listbox, here are some examples of different things you can do:
Use Images as Items
Stylized and Binding
Honestly it all depends on what you want it to look like. WPF is great in how much control it gives you on how something looks.
Crazy example using a listbox to make the planet's orbits

How to draw connecting lines between two controls on a grid WPF

I am creating controls (say button) on a grid. I want to create a connecting line between controls.
Say you you do mousedown on one button and release mouse over another button. This should draw a line between these two buttons.
Can some one help me or give me some ideas on how to do this?
Thanks in advance!
I'm doing something similar; here's a quick summary of what I did:
Drag & Drop
For handling the drag-and-drop between controls there's quite a bit of literature on the web (just search WPF drag-and-drop). The default drag-and-drop implementation is overly complex, IMO, and we ended up using some attached DPs to make it easier (similar to these). Basically, you want a drag method that looks something like this:
private void onMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
UIElement element = sender as UIElement;
if (element == null)
return;
DragDrop.DoDragDrop(element, new DataObject(this), DragDropEffects.Move);
}
On the target, set AllowDrop to true, then add an event to Drop:
private void onDrop(object sender, DragEventArgs args)
{
FrameworkElement elem = sender as FrameworkElement;
if (null == elem)
return;
IDataObject data = args.Data;
if (!data.GetDataPresent(typeof(GraphNode))
return;
GraphNode node = data.GetData(typeof(GraphNode)) as GraphNode;
if(null == node)
return;
// ----- Actually do your stuff here -----
}
Drawing the Line
Now for the tricky part! Each control exposes an AnchorPoint DependencyProperty. When the LayoutUpdated event is raised (i.e. when the control moves/resizes/etc), the control recalculates its AnchorPoint. When a connecting line is added, it binds to the DependencyProperties of both the source and destination's AnchorPoints. [EDIT: As Ray Burns pointed out in the comments the Canvas and grid just need to be in the same place; they don't need to be int the same hierarchy (though they may be)]
For updating the position DP:
private void onLayoutUpdated(object sender, EventArgs e)
{
Size size = RenderSize;
Point ofs = new Point(size.Width / 2, isInput ? 0 : size.Height);
AnchorPoint = TransformToVisual(node.canvas).Transform(ofs);
}
For creating the line class (can be done in XAML, too):
public sealed class GraphEdge : UserControl
{
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register("Source", typeof(Point), typeof(GraphEdge), new FrameworkPropertyMetadata(default(Point)));
public Point Source { get { return (Point) this.GetValue(SourceProperty); } set { this.SetValue(SourceProperty, value); } }
public static readonly DependencyProperty DestinationProperty = DependencyProperty.Register("Destination", typeof(Point), typeof(GraphEdge), new FrameworkPropertyMetadata(default(Point)));
public Point Destination { get { return (Point) this.GetValue(DestinationProperty); } set { this.SetValue(DestinationProperty, value); } }
public GraphEdge()
{
LineSegment segment = new LineSegment(default(Point), true);
PathFigure figure = new PathFigure(default(Point), new[] { segment }, false);
PathGeometry geometry = new PathGeometry(new[] { figure });
BindingBase sourceBinding = new Binding {Source = this, Path = new PropertyPath(SourceProperty)};
BindingBase destinationBinding = new Binding { Source = this, Path = new PropertyPath(DestinationProperty) };
BindingOperations.SetBinding(figure, PathFigure.StartPointProperty, sourceBinding);
BindingOperations.SetBinding(segment, LineSegment.PointProperty, destinationBinding);
Content = new Path
{
Data = geometry,
StrokeThickness = 5,
Stroke = Brushes.White,
MinWidth = 1,
MinHeight = 1
};
}
}
If you want to get a lot fancier, you can use a MultiValueBinding on source and destination and add a converter which creates the PathGeometry. Here's an example from GraphSharp. Using this method, you could add arrows to the end of the line, use Bezier curves to make it look more natural, route the line around other controls (though this could be harder than it sounds), etc., etc.
See also
http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/dd246675-bc4e-4d1f-8c04-0571ea51267b
http://www.codeproject.com/KB/WPF/WPFDiagramDesigner_Part1.aspx
http://www.codeproject.com/KB/WPF/WPFDiagramDesigner_Part2.aspx
http://www.codeproject.com/KB/WPF/WPFDiagramDesigner_Part3.aspx
http://www.codeproject.com/KB/WPF/WPFDiagramDesigner_Part4.aspx
http://www.syncfusion.com/products/user-interface-edition/wpf/diagram
http://www.mindscape.co.nz/products/wpfflowdiagrams/

Resources