WPF: fast way to apply formatting to RichTextBox - wpf

I'm trying to display basic syntax highlighting in a WPF RichTextBox. It mostly works, but the rendering performance is awful.
First I naively tried:
/// <summary>
/// Main event handler for syntax highlighting.
/// </summary>
private void XmlChanged(object sender, TextChangedEventArgs e)
{
VM.Dirty = true;
if (VM.Pretty)
{
var range = new TextRange(XmlView.Document.ContentStart, XmlView.Document.ContentEnd);
Render(range.Text);
}
}
/// <summary>
/// Entry point for programmatically resetting the textbox contents
/// </summary>
private void Render(string text)
{
XmlView.TextChanged -= this.XmlChanged;
if (VM.Pretty)
{
var tokens = tokenizer.Tokenize(text);
Format(XmlView.Document, tokens);
}
XmlView.TextChanged += this.XmlChanged;
}
private void Format(FlowDocument doc, List<Token> tokens)
{
var start = doc.ContentStart;
foreach (var token in tokens)
{
TextRange range = new TextRange(start.GetPositionAtOffset(token.StartPosition, LogicalDirection.Forward),
start.GetPositionAtOffset(token.EndPosition, LogicalDirection.Forward));
range.ApplyPropertyValue(TextElement.ForegroundProperty, m_syntaxColors[token.Type]);
}
}
Testing on a 2KB document with just over 100 tokens, it took 1-2 seconds to redraw after every keystroke; clearly not acceptable. Profiling showed that my tokenizer was orders of magnitude faster than the Format() function. So I tried some double-buffering:
private void Render(string text)
{
XmlView.TextChanged -= this.XmlChanged;
// create new doc offscreen
var doc = new FlowDocument();
var range = new TextRange(doc.ContentStart, doc.ContentEnd);
range.Text = text;
if (VM.Pretty)
{
var tokens = tokenizer.Tokenize(text);
Format(doc, tokens);
}
// copy to active buffer
var stream = new MemoryStream(65536);
range.Save(stream, DataFormats.XamlPackage);
var activeRange = new TextRange(XmlView.Document.ContentStart, XmlView.Document.ContentEnd);
activeRange.Load(stream, DataFormats.XamlPackage);
XmlView.TextChanged += this.XmlChanged;
}
Benchmarks show that Format() runs slightly faster rendering offscreen, but the perceived performance is even worse now!
What's the right way to go about this?

I'd try taking as much object instantiation out of the method/loop as possible and pass in references instead. You're calling new quite a few times per loop per keystroke.

Related

Winforms pie chart legend text length affects label and chartarea size

I have the following ChartArea Annotation settings set up:
private void chart1_PrePaint(object sender, ChartPaintEventArgs e)
{
if (e.ChartElement is ChartArea)
{
var ta = new TextAnnotation();
ta.IsMultiline = true;
ta.Text = "Results of Calculation\n%";
ta.Width = e.Position.Width;
ta.Height = e.Position.Height;
ta.X = e.Position.X;
ta.Y = e.Position.Y;
ta.Font = new Font("Candara", e.Position.Height / 10, FontStyle.Regular);
chart1.Annotations.Add(ta);
}
}
A few issues with this, and with the Legend in relation to my other posted question:
My other Pie Chart Legend/ChartArea question
With this PrePaint setup, I'm not sure if my position is correct for the TextAnnotation. I'm using the e.Position but it's coming out not "exactly" centered in the middle of the doughnut of the pie chart area. I'd like it to be centered perfectly. Not sure what other property to use here.
A second issue is that when Legend text length changes, it "pushes" and makes the ChartArea itself smaller so the pie chart gets smaller. I'd like it to be the other way around, where the ChartArea pie chart stays the same size but the Legend gets pushes aside.
Is this possible?
The following is the position setup of the pie chart:
Thanks
I'm sorry I couldn't help more, last time. I tested the centering of the TextAnnotation and in fact it has problems when the InnerPlotPosition is set to auto. Moreover, the answer found at link creates a new instance of the TextAnnotation at every PrePaint, causing the overlapping of TextAnnotations and the blurrying of the centered text.
I couldn't find a way to avoid the resizing of the doughnut (I'm not sure it's even possible, at this point...I'll wait for some other answers) but maybe this can work out as a workaround.
First I created a dictionary to store the centered TextAnnotations references (the key is the graph name, in case you have more than one), then in the PrePaint event I get the correct reference of the TextAnnotation used in the graph and update the coordinates of that one.
Second, I set the InnerPlotPosition manually, this seems to solve the problem of the centering of the TextAnnotation. Of course, you need to specify coordinates and size for the InnerPlot like I did with the line:
chart1.ChartAreas[0].InnerPlotPosition = new ElementPosition(0, 0, 60.65f, 94.99f);
Lastly, I set the position and the size of the legend manually and, with the extension method WrapAt I set a "line break" every _maxLegendTextBeforeWrap in the legend items text. Couldn't find a way to make it dynamically change with the width of the legend area, so it has to be set manually.
Below there's a GIF of the resulting effect. Don't know if this suits you as a solution (too much tweaking and code, for my taste), but anyway. Maybe this can trigger some new ideas on how to solve.
To do so I created these global variables:
/// <summary>
/// Saves the currently doughnut centered annotations per graph.
/// </summary>
private IDictionary<string, TextAnnotation> _annotationsByGraph;
/// <summary>
/// Number of characters
/// </summary>
private int _maxLegendTextBeforeWrap = 10;
/// <summary>
/// Legend area width.
/// </summary>
private int _legendWidth = 20;
/// <summary>
/// Legend area height.
/// </summary>
private int _legendHeight = 90;
This is the handler of the Load event:
private void ChartTest_Load(object sender, EventArgs e)
{
// ** Start of test data **
chart1.Series["Series1"].Points.AddXY("A", 33);
chart1.Series["Series1"].Points[0].LegendText = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
chart1.Series["Series1"].Points.AddXY("B", 33);
chart1.Series["Series1"].Points[1].LegendText = "BBBBBBBBBBBBBBBBBBBB";
chart1.Series["Series1"].Points.AddXY("C", 34);
chart1.Series["Series1"].Points[2].LegendText = "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC";
// ** End of test data **
// Creates a new instance of the dictionary storing the references to the annotations.
_annotationsByGraph = new Dictionary<string, TextAnnotation>();
// Createa a new instance of an annotation for the chart1 graph.
_annotationsByGraph.Add(chart1.Name, new TextAnnotation());
// Manually setting the position of the chart area prevents the imperfect positioning of the
// TextAnnotation at the center of the doughnut.
chart1.ChartAreas[0].Position.Auto = true;
// Manually set the position of the InnerPlotPosition area prevents the imperfect positioning of the
// TextAnnotation at the center of the doughnut.
chart1.ChartAreas[0].InnerPlotPosition.Auto = false;
chart1.ChartAreas[0].InnerPlotPosition = new ElementPosition(0, 0, 60.65f, 94.99f);
// Minimum size for the legend font.
chart1.Legends[0].AutoFitMinFontSize = 5;
// Set the legend style as column.
chart1.Legends[0].LegendStyle = LegendStyle.Column;
// Splits the legend texts with the space char every _maxLegendTextBeforeWrap characters.
chart1.Series["Series1"].Points.ToList().ForEach(p => p.LegendText = p.LegendText.WrapAt(_maxLegendTextBeforeWrap));
}
This is the handler of the PrePaint event:
private void chart1_PrePaint(object sender, ChartPaintEventArgs e)
{
if (e.ChartElement is ChartArea)
{
// Get the reference to the corresponding text annotation for this chart.
// We need this, otherwise we are creating and painting a new instance of a TextAnnotation
// at every PrePaint, with the resulting blurrying effect caused by the overlapping of the text.
var ta = _annotationsByGraph[e.Chart.Name];
// Check if the annotation has already been added to the chart.
if (!e.Chart.Annotations.Contains(ta))
e.Chart.Annotations.Add(ta);
// Set the properties of the centered TextAnnotation.
ta.IsMultiline = true;
ta.Text = "Results of Calculation\nx%";
ta.Font = new Font("Candara", e.Position.Height / 10, FontStyle.Regular);
ta.Width = e.Position.Width;
ta.Height = e.Position.Height;
ta.X = e.Position.X;
ta.Y = e.Position.Y;
// Move the legend manually to the right of the doughnut.
e.Chart.Legends[0].Position = new ElementPosition(e.Position.X + e.Position.Width, e.Position.Y, _legendWidth, _legendHeight);
}
}
This is what the button does:
private void BtnChangeLegendItemLength_Click(object sender, EventArgs e)
{
if (chart1.Series["Series1"].Points[1].LegendText.StartsWith("DD"))
chart1.Series["Series1"].Points[1].LegendText = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".WrapAt(_maxLegendTextBeforeWrap);
else
chart1.Series["Series1"].Points[1].LegendText = "DDDDDD".WrapAt(_maxLegendTextBeforeWrap);
}
This is the extension method definition:
internal static class ExtensionMethods
{
public static string WrapAt(this string legendText, int maxLengthBeforeWrap)
{
if (legendText.Length <= maxLengthBeforeWrap)
return legendText;
// Integer division to get how many times we have to insert a space.
var times = legendText.Length / maxLengthBeforeWrap;
// Counter of added spaces.
var spacesAdded = 0;
// Iterate for each space char needed.
for (var i = 1; i <= times; i++)
{
// Insert a space char every maxLengthBeforeWrap positions.
legendText = legendText.Insert(maxLengthBeforeWrap * i + spacesAdded, new string(' ', 1));
spacesAdded++;
}
return legendText;
}
}

WPF enqueue and replay key events

I'm trying to improve the responsiveness of a WPF business application so that when users are "between" screens waiting for a new screen to appear after a server response, they can still be entering data. I'm able to queue the events (using a PreviewKeyDown event handler on background panel) but then I'm having difficulties just throwing the events I dequeue back at the new panel once it's loaded. In particular TextBoxes on the new panel are not picking up the text. I've tried raising the same events (setting Handled to true when capturing them, setting Handled to false when raising them again) creating new KeyDown events, new PreviewKeyDown events, doing ProcessInput, doing RaiseEvent on the panel, setting the focus on the right TextBox and doing RaiseEvent on the TextBox, many things.
It seems like it should be really simple, but I can't figure it out.
Here are some of the things I've tried. Consider a Queue of KeyEventArgs called EventQ:
Here's one thing that doesn't work:
while (EventQ.Count > 0)
{
KeyEventArgs kea = EventQ.Dequeue();
tbOne.Focus(); // tbOne is a text box
kea.Handled = false;
this.RaiseEvent(kea);
}
Here's another:
while (EventQ.Count > 0)
{
KeyEventArgs kea = EventQ.Dequeue();
tbOne.Focus(); // tbOne is a text box
var key = kea.Key; // Key to send
var routedEvent = Keyboard.PreviewKeyDownEvent; // Event to send
KeyEventArgs keanew = new KeyEventArgs(
Keyboard.PrimaryDevice,
PresentationSource.FromVisual(this),
0,
key) { RoutedEvent = routedEvent, Handled = false };
InputManager.Current.ProcessInput(keanew);
}
And another:
while (EventQ.Count > 0)
{
KeyEventArgs kea = EventQ.Dequeue();
tbOne.Focus(); // tbOne is a text box
var key = kea.Key; // Key to send
var routedEvent = Keyboard.PreviewKeyDownEvent; // Event to send
this.RaiseEvent(
new KeyEventArgs(
Keyboard.PrimaryDevice,
PresentationSource.FromVisual(this),
0,
key) { RoutedEvent = routedEvent, Handled = false }
);
}
One strange thing I've noticed is that when using the InputManager method (#2) spaces do appear. But normal text keys do not.
The same resources turned up for me when I did some research, so I think what you do in your answer is pretty valid.
I looked on and have found another way of doing it, using the Win32 API. I had to introduce some threading and small delays, because for some reason the key events were not replayed in the correct sequence without that. Overall I think this solution is easier though, and I also figured out how to include modifier keys (by using the Get/SetKeyboardState function). Uppercase is working, and so should keyboard shortcuts.
Starting the demo app, pressing the keys 1 space 2 space 3 tab 4 space 5 space 6, then clicking the button produces the following:
Xaml:
<UserControl x:Class="WpfApplication1.KeyEventQueueDemo"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300" >
<StackPanel>
<TextBox x:Name="tbOne" Margin="5,2" />
<TextBox x:Name="tbTwo" Margin="5,2" />
<Button x:Name="btn" Content="Replay key events" Margin="5,2" />
</StackPanel>
</UserControl>
Code behind:
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;
namespace WpfApplication1
{
/// <summary>
/// Structure that defines key input with modifier keys
/// </summary>
public struct KeyAndState
{
public int Key;
public byte[] KeyboardState;
public KeyAndState(int key, byte[] state)
{
Key = key;
KeyboardState = state;
}
}
/// <summary>
/// Demo to illustrate storing keyboard input and playing it back at a later stage
/// </summary>
public partial class KeyEventQueueDemo : UserControl
{
private const int WM_KEYDOWN = 0x0100;
[DllImport("user32.dll")]
static extern bool PostMessage(IntPtr hWnd, UInt32 Msg, int wParam, int lParam);
[DllImport("user32.dll")]
static extern bool GetKeyboardState(byte[] lpKeyState);
[DllImport("user32.dll")]
static extern bool SetKeyboardState(byte[] lpKeyState);
private IntPtr _handle;
private bool _isMonitoring = true;
private Queue<KeyAndState> _eventQ = new Queue<KeyAndState>();
public KeyEventQueueDemo()
{
InitializeComponent();
this.Focusable = true;
this.Loaded += KeyEventQueueDemo_Loaded;
this.PreviewKeyDown += KeyEventQueueDemo_PreviewKeyDown;
this.btn.Click += (s, e) => ReplayKeyEvents();
}
void KeyEventQueueDemo_Loaded(object sender, RoutedEventArgs e)
{
this.Focus(); // necessary to detect previewkeydown event
SetFocusable(false); // for demo purpose only, so controls do not get focus at tab key
// getting window handle
HwndSource source = (HwndSource)HwndSource.FromVisual(this);
_handle = source.Handle;
}
/// <summary>
/// Get key and keyboard state (modifier keys), store them in a queue
/// </summary>
void KeyEventQueueDemo_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (_isMonitoring)
{
int key = KeyInterop.VirtualKeyFromKey(e.Key);
byte[] state = new byte[256];
GetKeyboardState(state);
_eventQ.Enqueue(new KeyAndState(key, state));
}
}
/// <summary>
/// Replay key events from queue
/// </summary>
private void ReplayKeyEvents()
{
_isMonitoring = false; // no longer add to queue
SetFocusable(true); // allow controls to take focus now (demo purpose only)
MoveFocus(new TraversalRequest(FocusNavigationDirection.Next)); // set focus to first control
// thread the dequeueing, because the sequence of inputs is not preserved
// unless a small delay between them is introduced. Normally the effect this
// produces should be very acceptable for an UI.
Task.Run(() =>
{
while (_eventQ.Count > 0)
{
KeyAndState keyAndState = _eventQ.Dequeue();
Application.Current.Dispatcher.BeginInvoke((Action)(() =>
{
SetKeyboardState(keyAndState.KeyboardState); // set stored keyboard state
PostMessage(_handle, WM_KEYDOWN, keyAndState.Key, 0);
}));
System.Threading.Thread.Sleep(5); // might need adjustment
}
});
}
/// <summary>
/// Prevent controls from getting focus and taking the input until requested
/// </summary>
private void SetFocusable(bool isFocusable)
{
tbOne.Focusable = isFocusable;
tbTwo.Focusable = isFocusable;
btn.Focusable = isFocusable;
}
}
}
The enqueue system is something that I've wanted to do myself, as part of my project which allows multi-threaded UI to function without any problems(one thread routes events into another). There is only slight problem, namely WPF does not have public API to inject INPUT events. Here is a copy/paste from one of the Microsoft employees that I talked with, like weeks back:
"WPF does not expose public methods for injecting input events in the proper way. This scenario is just not supported by the public API. You will probably have to do a lot of reflection and other hacking. For example, WPF treats some input as “trusted” because it knows it came from the message pump. If you just raise an input event, the event will not be trusted."
I think you need to rethink your strategy.
Thanks all for your support but I haven't really struck a solution from the SO community so I'm going to answer this myself since this is the closest I seem to get to a solution. The "hack" as Erti-Chris says seems to be what we're left with. I've had some luck decomposing the problem so I don't have the sense I'm writing a whole new keyboard handler. The approach I'm following is to decompose the events into a combination of InputManager handling and of TextComposition. Throwing a KeyEventArgs (either the original one or one I've created myself) doesn't seem to register on a PreviewKeyDown handler.
Part of the difficulty comes from the information in Erti-Chris's post, and another part seems to be related to TextBoxes trying to react to certain keys like arrow keys differently from normal keys like the letter "A".
To move forward with this I found information from this post to be useful:
http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/b657618e-7fc6-4e6b-9b62-1ffca25d186b
Here is the solution that I'm getting some positive results from now:
Keyboard.Focus(tbOne); // the first element on the Panel to get the focus
while (EventQ.Count > 0)
{
KeyEventArgs kea = EventQ.Dequeue();
kea.Handled = false;
var routedEvent = KeyDownEvent;
KeyEventArgs keanew = new KeyEventArgs(
Keyboard.PrimaryDevice,
PresentationSource.FromVisual(tbOne),
kea.Timestamp,
kea.Key) { RoutedEvent = routedEvent, Handled = false };
keanew.Source = tbOne;
bool itWorked = InputManager.Current.ProcessInput(keanew);
if (itWorked)
{
continue;
// at this point spaces, backspaces, tabs, arrow keys, deletes are handled
}
else
{
String keyChar = kea.Key.ToString();
if (keyChar.Length > 1)
{
// handle special keys; letters are one length
if (keyChar == "OemPeriod") keyChar = ".";
if (keyChar == "OemComma") keyChar = ",";
}
TextCompositionManager.StartComposition(new TextComposition(InputManager.Current, Keyboard.FocusedElement, keyChar));
}
}
If anyone can show me a better way I'm delighted to mark your contribution as the answer, but for now this is what I'm working with.

Working with ProgressBar and ComboBox

I'm in trouble with a Marquee ProgressBar. I need to execute a method (refreshList()) to get a List<string>. Then I assign this List to a ComboBox, so ComboBox refreshes with the new Items. As refreshList() take 3 or 4 sec, I wanted to run a Marquee ProgressBar. But I couldn't. ProgressBar is ok, but ComboBox doesn't load new Items.
My refreshList() method:
private void refreshList(List<string> list)
{
albumList.DataSource = null;
albumList.DataSource = list;
}
I have the following code, it works fine:
private void changeDirectoryToolStripMenuItem_Click(object sender, EventArgs e)
{
fbd.RootFolder = Environment.SpecialFolder.MyComputer;
folderPath = "";
if (fbd.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
folderPath = fbd.SelectedPath;
refreshList(N.getList(folderPath));
}
}
But I added a ProgressBar and wrote this code:
private void changeDirectoryToolStripMenuItem_Click(object sender, EventArgs e)
{
fbd.RootFolder = Environment.SpecialFolder.MyComputer;
folderPath = "";
if (fbd.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
folderPath = fbd.SelectedPath;
bgWorker.WorkerReportsProgress = true;
bgWorker.RunWorkerAsync();
}
}
And I placed refreshList() in doWork() method:
private void bgWorker_DoWork(object sender, DoWorkEventArgs e)
{
refreshList(N.getList(folderPath));
}
But unfortunately this isn't working. Can anybody help me solving this problem? Thanks in advance.
You can use the MarqueeAnimationSpeed and Value properties of the ProgressBar control to stop and start the Marquee. There's no need to use WorkerReportsProgress* as you aren't incrementing a normal progress bar - you just want to "spin" the Marquee.
You can do something like the following:
public Form1()
{
InitializeComponent();
//Stop the progress bar to begin with
progressBar1.MarqueeAnimationSpeed = 0;
//If you wire up the event handler in the Designer, then you don't need
//the following line of code (the designer adds it to InitializeComponent)
//backgroundWorker1.RunWorkerCompleted += backgroundWorker1_RunWorkerCompleted;
}
private void changeDirectoryToolStripMenuItem_Click(object sender, EventArgs e)
{
fbd.RootFolder = Environment.SpecialFolder.MyComputer;
folderPath = "";
if (fbd.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
folderPath = fbd.SelectedPath;
//This line effectively starts the progress bar
progressBar1.MarqueeAnimationSpeed = 10;
bgWorker.RunWorkerAsync(); //Calls the DoWork event
}
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
e.Result = N.getList(folderPath); //Technically this is the only work you need to do in the background
}
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
//these two lines effectively stop the progress bar
progressBar1.Value = 0;
progressBar1.MarqueeAnimationSpeed = 0;
//Now update the list with the result from the work done on the background thread
RefreshList(e.Result as List<String>);
}
private void RefreshList(List<String> results)
{
albumList.DataSource = null; //You don't need this line but there is no real harm.
albumList.DataSource = list;
}
Remember to wire up the RunWorkerCompleted event to backgroundWorker1_RunWorkerCompleted via the Properties bar, Events section in the designer.
To begin with, we start the ProgressBar's animation by setting the MarqueeAnimationSpeed property to a non-zero positive number as part of your successful folder selection.
Then, after calling RunWorkerAsync, the code builds your list in the DoWork method, then assigns the result to the DoWorkEventArgs, which get passed to the RunWorkerCompleted event (which fires when DoWork is finished).
In the backgroundWorker1_RunWorkerCompleted method, we stop the progress bar (and set it's value to zero to effectively return it to it's original state), and then we pass the list to the refreshList method to databind it and populate the ComboBox.
Tested using VS2012, Windows Forms, .Net 4.0 (with a Thread.Sleep to emulate the time taken for N.getList)
*WorkerReportsProgress, and the associated ReportProgress method/event are used when you want to increment the progress bar - you can tell the GUI that you are 10% done, 20% done, 50% done etc etc.

Setting ToolStripDropDown.DropShadowEnabled to false on multi level dropdowns

I want to disable the dropdown shadow on the dropdown of a ToolStripDropDownButton. If the dropdown menu contains items that have dropdowns themselves (e.g. multi-level menu) then setting the DropShadowEnabled to false on the ToolStripDropDownButton causes the top level dropdown to appear at the wrong position. See attached picture.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
toolStripDropDownButton1.DropDown.DropShadowEnabled = false;
}
}
partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1));
this.toolStrip1 = new System.Windows.Forms.ToolStrip();
this.toolStripDropDownButton1 = new System.Windows.Forms.ToolStripDropDownButton();
this.item1ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.subitem1ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStrip1.SuspendLayout();
this.SuspendLayout();
//
// toolStrip1
//
this.toolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.toolStripDropDownButton1});
this.toolStrip1.Location = new System.Drawing.Point(0, 0);
this.toolStrip1.Name = "toolStrip1";
this.toolStrip1.Size = new System.Drawing.Size(292, 25);
this.toolStrip1.TabIndex = 0;
this.toolStrip1.Text = "toolStrip1";
//
// toolStripDropDownButton1
//
this.toolStripDropDownButton1.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
this.toolStripDropDownButton1.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.item1ToolStripMenuItem});
this.toolStripDropDownButton1.Image = ((System.Drawing.Image)(resources.GetObject("toolStripDropDownButton1.Image")));
this.toolStripDropDownButton1.ImageTransparentColor = System.Drawing.Color.Magenta;
this.toolStripDropDownButton1.Name = "toolStripDropDownButton1";
this.toolStripDropDownButton1.Size = new System.Drawing.Size(29, 22);
this.toolStripDropDownButton1.Text = "toolStripDropDownButton1";
//
// item1ToolStripMenuItem
//
this.item1ToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.subitem1ToolStripMenuItem});
this.item1ToolStripMenuItem.Name = "item1ToolStripMenuItem";
this.item1ToolStripMenuItem.Size = new System.Drawing.Size(152, 22);
this.item1ToolStripMenuItem.Text = "item1";
//
// subitem1ToolStripMenuItem
//
this.subitem1ToolStripMenuItem.Name = "subitem1ToolStripMenuItem";
this.subitem1ToolStripMenuItem.Size = new System.Drawing.Size(152, 22);
this.subitem1ToolStripMenuItem.Text = "subitem1";
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(292, 273);
this.Controls.Add(this.toolStrip1);
this.Name = "Form1";
this.Text = "Form1";
this.toolStrip1.ResumeLayout(false);
this.toolStrip1.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.ToolStrip toolStrip1;
private System.Windows.Forms.ToolStripDropDownButton toolStripDropDownButton1;
private System.Windows.Forms.ToolStripMenuItem item1ToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem subitem1ToolStripMenuItem;
}
This is very typical lossage in the ToolStripItem classes. Clearly it is a bug, it probably got introduced when they applied a hack to work around a Windows problem. You can still see the internal bug number in the Reference Source:
public bool DropShadowEnabled {
get {
// VSWhidbey 338272 - DropShadows are only supported on TopMost windows
// due to the flakeyness of the way it's implemented in the OS. (Non toplevel
// windows can have parts of the shadow disappear because another window can get
// sandwiched between the SysShadow window and the dropdown.)
return dropShadowEnabled && TopMost && DisplayInformation.IsDropShadowEnabled;
}
set { // etc... }
}
But without a corresponding fix in the setter and the renderer.
The flakeyness they mentioned actually got fixed in Vista, you are still running on XP so you can't see it. Drop shadows are done differently on the Aero desktop and it is a system setting whether or not they are enabled. So using the property is entirely ineffective on Aero.
These ToolStripItem class bugs didn't get fixed after the .NET 2.0 release, about the entire Winforms team moved over to the WPF group. And they are certainly not getting fixed now, no point filing a bug at connect.microsoft.com although you are free to do so.
With the added wrinkle that the property just cannot have an effect anymore on later versions of Windows since it is now a system setting, the only logical thing to do here is to throw in the towel. Don't change the property.

WPF Datagrid: copy additional rows to clipboard

I am working with the WPF Datagrid and I am trying to enhance/change the copy & paste mechanism.
When the user selects some cells and then hit CTRL + C, the underlying controls is able to catch the CopyingRowClipboardContent event.
this.mainDataGrid.CopyingRowClipboardContent
+= this.DatagridOnCopyingRowClipboardContent;
In this method, some cells are added to both the header and the rows, hence resulting in a "wider" grid.
private void DatagridOnCopyingRowClipboardContent(
object sender,
DataGridRowClipboardEventArgs dataGridRowClipboardEventArgs)
{
// this is fired every time a row is copied
var allContent = dataGridRowClipboardEventArgs.ClipboardRowContent;
allContent.Insert(0, new DataGridClipboardCellContent(
null,
this.mainDataGrid.Columns[0],
"new cell"));
}
At this point I am stuck because I am trying to add an additional row before the header and two after the last row (see image below).
Any ideas? Suggestions?
Please note I am not interested in an MVVM way of doing it here.
Here is a code snippet that might help you.
This snippet is mainly used to retrieve all of your selected data, including headers (I removed the RowHeaders part since you apparently don't need it).
If you have any question please let me know. I left a few part with comments written in capital letters: this is where you should add your own data
The good part of this approach is that it directly works with your DataGrid's ItemsSource and NOT the DataGridCell. The main reason being: if you use DataGridCell on a formatted number for example, you will NOT get the actual value, but just the formatted one (say your source is 14.49 and your StringFormat is N0, you'll just copy 14 if you use a "regular" way)
/// <summary>
/// Handles DataGrid copying with headers
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnCopyingDataGrid(object sender, ExecutedRoutedEventArgs e)
{
// First step: getting the coordinates list of all cells selected
IList<Tuple<int, int>> cellsCoordinatesList = new List<Tuple<int, int>>();
HashSet<int> rowList = new HashSet<int>();
HashSet<int> columnList = new HashSet<int>();
foreach (System.Windows.Controls.DataGridCellInfo cell in this.SelectedCells)
{
int column = cell.Column.DisplayIndex;
int row = this.Items.IndexOf(cell.Item);
cellsCoordinatesList.Add(new Tuple<int, int>(row, column));
if (!rowList.Contains(row))
{
rowList.Add(row);
}
if (!columnList.Contains(column))
{
columnList.Add(column);
}
}
// Second step: Create the table to copy/paste
object[,] arrayToBeCopied = new object[rowList.Count, columnList.Count + 1];
IList<string> colHead = this.ColumnHeaders.Cast<object>().Select(h => h.ToString()).ToList();
for (int row = 0; row < arrayToBeCopied.GetLength(0); row++)
{
for (int column = 0; column < arrayToBeCopied.GetLength(1); column++)
{
if (row == 0)
{
arrayToBeCopied[row, column] = colHead[columnList.ElementAt(column - 1)];
}
else
{
arrayToBeCopied[row, column] = // WHATEVER YOU WANT TO PUT IN THE CLIPBOARD SHOULD BE HERE. THIS SHOULD GET SOME PROPERTY IN YOUR ITEMSSOURCE
}
}
}
// Third step: Converting it into a string
StringBuilder sb = new StringBuilder();
// HERE, ADD YOUR FIRST ROW BEFORE STARTING TO PARSE THE COPIED DATA
for (int row = 0; row < arrayToBeCopied.GetLength(0); row++)
{
for (int column = 0; column < arrayToBeCopied.GetLength(1); column++)
{
sb.Append(arrayToBeCopied[row, column]);
if (column < arrayToBeCopied.GetLength(1) - 1)
{
sb.Append("\t");
}
}
sb.Append("\r\n");
}
// AND HERE, ADD YOUR LAST ROWS BEFORE SETTING THE DATA TO CLIPBOARD
DataObject data = new DataObject();
data.SetData(DataFormats.Text, sb.ToString());
Clipboard.SetDataObject(data);
}
Are you trying to copy the content into e.g. Excel later?
If so, here's what I did:
/// <summary>
/// Copy the data from the data grid to the clipboard
/// </summary>
private void copyDataOfMyDataGridToClipboard(object sender, EventArgs e)
{
// Save selection
int selectedRow = this.myDataGrid.SelectedRows[0].Index;
// Select data which you would like to copy
this.myDataGrid.MultiSelect = true;
this.myDataGrid.SelectAll();
// Prepare data to be copied (that's the interesting part!)
DataObject myGridDataObject = this.myDataGrid.GetClipboardContent();
string firstRow = "FirstRowCommentCell1\t"+ this.someDataInCell2 +"..\r\n";
string lastTwoRows = "\r\nBottomLine1\t" + yourvariables + "\r\nBottomLine2";
string concatenatedData = firstRow + myGridDataObject.GetText() + lastTwoRows;
// Copy into clipboard
Clipboard.SetDataObject(concatenatedData);
// Restore settings
this.myDataGrid.ClearSelection();
this.myDataGrid.MultiSelect = false;
// Restore selection
this.myDataGrid.Rows[selectedRow].Selected = true;
}
In my case I had some static header's which could be easily concatenated with some variables. Important to write are the \t for declaring another cell, \r\n declares the next row
I realize this an older post, but I post this solution for completeness. I could not find a more recent question on copying datagrid rows to clipboard. Using Clipboard.SetData belies the ClipboardRowContent intention.
For my needs, I'm re-pasting back into the e.ClipboardRowContent the row I would like. The cell.Item is all the information I need for each selected row.
Hint: I was getting duplicates without doing an e.ClipboardRowContent.Clear(); after using the e.ClipboardRowContent . I was clearing before and using DataGrid.SelectedItems to build the rows.
private void yourDataGrid_CopyingRowClipboardContent(object sender, DataGridRowClipboardEventArgs e)
{
var dataGridClipboardCellContent = new List<DataGridClipboardCellContent>();
string prevCell = "";
string curCell = "";
foreach (DataGridClipboardCellContent cell in e.ClipboardRowContent)
{
//Gives you access item.Item or item.Content here
//if you are using your struct (data type) you can recast it here curItem = (yourdatatype)item.Item;
curItem = cell.Item.ToString();
if (curCell != prevCell)
dataGridClipboardCellContent.Add(new DataGridClipboardCellContent(item, item.Column, curCell));
prevCell = curCell;
}
e.ClipboardRowContent.Clear();
//Re-paste back into e.ClipboardRowContent, additionally if you have modified/formatted rows to your liking
e.ClipboardRowContent.AddRange(dataGridClipboardCellContent);
}

Resources