WPF RichTextBox as a text queue for logging purposes - c#

I would like to use a RichTextBox WPF control to print log outputs, but I am wondering what is the most efficient way to remove the "oldest" lines after there are for example more than 10,000 lines displayed, if it is possible to actually implement a queue behaviour, especially as there is no easy "Text" property to play with.
Unfortunately, I am not able to achieve this result with nlog either probably due to a bug or a limitation.

You can use a Queue<string> to store your log messages, and display them using the RichTextBox.Lines property.
For example:
private const int _maxCapacity = 10000;
private Queue<string> _messageQueue = new Queue<string>(_maxCapacity);
private void button1_Click(object sender, EventArgs e)
{
if (_messageQueue.Count >= _maxCapacity)
{
_messageQueue.Dequeue();
}
_messageQueue.Enqueue("message " + _count++.ToString());
richTextBox1.Lines = _messageQueue.ToArray();
}
If you want the most recent messages to appear on top, reverse the queue:
richTextBox1.Lines = _messageQueue.Reverse().ToArray();

If you extend the original control with a LineCount int property, it is possible to use this (inspired by some code given here):
if (this.MaxLines > 0)
{
this.lineCount++;
if (this.lineCount > this.MaxLines)
{
tr = new TextRange(rtbx.Document.ContentStart, rtbx.Document.ContentEnd);
tr.Text = tr.Text.Remove(0, tr.Text.IndexOf('\n'));
this.lineCount--;
}
}
//And for auto scrolling
if (this.AutoScroll)
{
rtbx.ScrollToEnd();
}

Related

c# coding a timed operation within a for loop

I am writing a piece of code that takes a user through a guided script. The user will have a few seconds to answer before the answer will be displayed.
So far, my code looks like this:
GuidedExercise3 exercise3 = new GuidedExercise3();
string AntonioAnswer = string.Empty; // expected answer
int upperBound = exercise3.Script.Count - 1; // zero-based counting
for(int i = 0; i < upperBound; i += 2)
{
labelInstructions.Text = exercise3.Script[i].TextToSpeak;
AntonioAnswer = exercise3.Script[i+1].TextToSpeak; // answer
SetTimer(AntonioAnswer, txtAntonio); // set timer sending in the answer and the TextBox object.
sysTimer.Start();
}
The odd lines of a List contain the question and the even lines contain the expected answer. My question is how do I display the question for X seconds and then get the user's answer in this WinForms app and then display the answer when the timer elapses, keeping the user from going to the next step of the script but allowing them to answer the question (which is in a Textbox).
I examined this StackOverflow question, but it doesn't match: Implementing a loop using a timer in C#
Here's how I would handle something like this:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
MoveNextQuestion();
timerAnswer.Interval = 5000;
timerAnswer.Start();
}
private string[] _questionsAndAnswers = new[]
{
"What colour is the sky?",
"Blue",
"What do chickens lay?",
"Eggs",
};
private int _currentIndex = -2;
private void timerAnswer_Tick(object sender, EventArgs e)
{
MoveNextQuestion();
}
private void buttonAnswer_Click(object sender, EventArgs e)
{
MoveNextQuestion();
}
private void MoveNextQuestion()
{
_currentIndex += 2;
if (_currentIndex < _questionsAndAnswers.Length)
{
labelQuestion.Text = _questionsAndAnswers[_currentIndex];
}
else
{
timerAnswer.Stop();
}
}
}
I was able to get this working fairly easily with a BackgroundWorker object. See the following article at MSDN for the exact coding. BackgroundWorker Class. In particular they have two examples in the documentation and the first example is sufficient. The BackgroundWorker class is allowing my UI to continue to accept user input while waiting on a timed answer. It displays the correct answer on the RunWorkerComplete event. So the BackgroundWorker's RunAsync gets called in my for loop.
I've run into the additional issue of the BackgroundWorker not returning control to my loop. I'm looking into that problem separately.

Accelerate add text to a TextBox

I have code to set text of TexBox as
textBox1.Text = s;
where s is a string that have more than 100,000 char, and it take long time to show text on textBox.
Anybody have solution to make it faster ?
To do that split the s string into many strings, and use the AppendText to add those subStrings, if you check MSDN you will see that :
The AppendText method enables the user to append text to the contents of a text control without using text concatenation, which, can yield better performance when many concatenations are required.
public string s = "Put you terribly long string here";
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
//For responsiveness
textBox1.BeginInvoke(new Action(() =>
{
//Here's your logic
for (int i = 0; i < s.Length; i += 1000)
{
//This if is just for security
if (i+1000 > s.Length)
{
//Here's your AppendText
textBox1.AppendText(s.Substring(i, s.Length-i));
}
else
{
//And it's here as well
textBox1.AppendText(s.Substring(i, 1000));
}
}
}));
}
I used the value 1000, you can use 1500 , 2000 , choose the one that gives better result.
Hope this helps.
Update :
AppendText is available for both WindowsForms and WPF, too bad can't find it on WindowsPhone and WinRT. so I think this solution may help you a lot
break s in sub strings and when you pass first sub string to the text box it will appear after that it concatenate to the second and so on.
other way is that use loop to set the value
for(int i=0;i<s.length; i++)
{
textBox1.Text += s[i];
}
may these helps you

How to display data received from serial port in a textbox without the text disappearing in Visual Studio C#?

So, I'm trying to develop a simple application in visual C# which gets data from serial port and displays it in a textbox (to monitor temperature). I'm acquiring and displaying the data successfully, using the DataReceived event to update a global string variable and a timer to update the text field on my text box, as shown:
private void port_DataReceived_1(object sender, SerialDataReceivedEventArgs e)
{
try
{
globalVar.updateTemp = port.ReadLine(); //This is my global string
}
catch (IOException)
{
}
catch (InvalidOperationException)
{
}
catch (TimeoutException)
{
}
}
private void timer1_Tick(object sender, EventArgs e)
{
tempDisplayBox.Text = globalVar.updateTemp; //This is my textbox updating
}
The only issue I have is that the value shown in the textbox keeps flashing, making it hard to read. My timer is set to trigger every 10 ms (which should be fast enough, right?). Is there any way to make it more stable? I realize this may be a newb question, but to be fair I am a newb :) Any help is appreciated! Thanks!
Do you really need it updating every 10ms? What about every 500 ms or if not that then 100ms. 100ms will require your update method run 10 times less and therefore update 10 times less. The flickering you are expiriencing is due to the refresh speed. You could create custom method which will only update the temp only when target Label or textBox value is different than source port. But that will only sort the flickering when temp is steady, when temp will start vary it will bring back the flickering. Good luck ;-)
UPDATE
Hi I tried to reproduce the conditions and could not make my textbox nor Label flash. The way I tested it was by assigning int ntick = 0; and then increment the ++ntick; inside of the timer_tick method. The results didn't make any of the controls flash and were updated even every milisecond at some point. I also tried string.Format to put some load on the method. Is your app responsive?
The trick is to use double buffering. This way the operating system will redraw the Control off-screen, and only show the control when it is fully redrawn.
I have had the same problem, and solved it by extending the TextBox control like this:
public FastLogBox()
{
InitializeComponent();
_logBoxText = new StringBuilder(150000);
timer1.Interval = 20;
timer1.Tick += timer1_Tick;
timer1.Start();
SetStyle(ControlStyles.DoubleBuffer, true);
}
void timer1_Tick(object sender, EventArgs e)
{
if (_timeToClear)
{
_logBoxText.Clear();
_timeToClear = false;
}
if (_logQueue.Count <= 0) return;
while (!_logQueue.IsEmpty)
{
string element;
if (!_logQueue.TryDequeue(out element)) continue;
{
_logBoxText.Insert(0, element + "\r\n");
}
}
if (_logBoxText.Length > 150000)
{
_logBoxText.Remove(150000, _logBoxText.Length - 150001);
}
Text = _logBoxText.ToString();
}
public new void Clear()
{
_timeToClear = true;
while (!_logQueue.IsEmpty)
{
string element;
_logQueue.TryDequeue(out element);
}
}
public void AddToQueue(string message)
{
_logQueue.Enqueue(message);
}
}
I also use a timer and a concurrentQueue to avoid using Invoke to update the control from another thread. I also use a StringBuilder to prepare the string before putting it into the TextBox. StringBuilder is faster when building larger strings.
You can use ReadExisting() to read the whole data at a time.
You need to handle DataReceived Event of SerialPort
serialPort1.ReadExisting();
Sample:
private void serialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
String myData=serialPort1.ReadExisting();
}
Example Code: Here i would like to show you the code to Read Data(RFID Tag Code which is basically of length 12)
String macid = "";
private void DoWork()
{
Invoke(
new SetTextDeleg(machineExe ),
new object[] { macid });
macid = "";
}
private void serialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
string str1;
macid += serialPort1.ReadExisting();
if (macid.Length == 12)
{
macid = macid.Substring(0, 10);
Thread t = new Thread(new ThreadStart(DoWork));
t.Start();
}
}
public void machineExe(string text)
{
TextBox1.Text=text;
}
Thank you so much for the answers! I found a way to work around this issue:
Instead of replacing the contents of my textbox by rewriting the TextBox.Text property - which, as HenningNT implied, refreshes the control and causes the flickering - I'm now using the TextBox.AppendText method. Though, as I want to display only one line of data at a time, I use the textbox in multiline mode and the Environment.NewLine to jump to a new line before appending the text. As for the method of updating, I've gone back to using the timer because with the invoke method was crashing my application when I close the form, for some reason. Also, enabling double buffering didn't do me much good, although I guess I was doing it wrong... It still flickers a bit, but it's much better now :) I know this is not really a perfect solution (much more of a workaround), so I'll keep looking for it. If I find it, I'll be sure to update it here ;) My code:
private void timer1_Tick(object sender, EventArgs e) //Timer to update textbox
{
if (tempDisplayBox.Text != globalVar.updateTemp) //Only update if temperature is different
{
try
{
tempDisplayBox.AppendText(Environment.NewLine);
tempDisplayBox.AppendText(globalVar.updateTemp);
}
catch (NullReferenceException)
{
}
}
}

Slide object using For Loop (C#)

I did quite a bit of searching around and didn't find anything of much help.
Is it possible to "slide" or "move" using C#, an object from one Location to another using a simple For loop?
Thank you
I would suggest you rather use a Timer. There are other options, but this will be the simplist if you want to avoid threading issues etc.
Using a straight for loop will require that you pump the message queue using Application.DoEvents() to ensure that windows has the opportunity to actually render the updated control otherwise the for loop would run to completion without updating the UI and the control will appear to jump from the source location to the target location.
Here is a QAD sample for animating a button in the Y direction when clicked. This code assumes you put a timer control on the form called animationTimer.
private void button1_Click(object sender, EventArgs e)
{
if (!animationTimer.Enabled)
{
animationTimer.Interval = 10;
animationTimer.Start();
}
}
private int _animateDirection = 1;
private void animationTimer_Tick(object sender, EventArgs e)
{
button1.Location = new Point(button1.Location.X, button1.Location.Y + _animateDirection);
if (button1.Location.Y == 0 || button1.Location.Y == 100)
{
animationTimer.Stop();
_animateDirection *= -1; // reverse the direction
}
}
Assuming that the object you're talking about is some kind of Control you could just change the Location property of it.
So something like this:
for(int i = 0; i < 100; i++)
{
ctrl.Location.X += i;
}
Should work I think.

Google Suggestish text box (autocomplete)

What would be the best way to develop a text box that remembers the last x number of entries that were put into it. This is a standalone app written with C#.
This is actually fairly easy, especially in terms of showing the "AutoComplete" part of it. In terms of remembering the last x number of entries, you are just going to have to decide on a particular event (or events) that you consider as an entry being completed and write that entry off to a list... an AutoCompleteStringCollection to be precise.
The TextBox class has the 3 following properties that you will need:
AutoCompleteCustomSource
AutoCompleteMode
AutoCompleteSource
Set AutoCompleteMode to SuggestAppend and AutoCompleteSource to CustomSource.
Then at runtime, every time a new entry is made, use the Add() method of AutoCompleteStringCollection to add that entry to the list (and pop off any old ones if you want). You can actually do this operation directly on the AutoCompleteCustomSource property of the TextBox as long as you've already initialized it.
Now, every time you type in the TextBox it will suggest previous entries :)
See this article for a more complete example: http://www.c-sharpcorner.com/UploadFile/mahesh/AutoCompletion02012006113508AM/AutoCompletion.aspx
AutoComplete also has some built in features like FileSystem and URLs (though it only does stuff that was typed into IE...)
#Ethan
I forgot about the fact that you would want to save that so it wasn't a per session only thing :P But yes, you are completely correct.
This is easily done, especially since it's just basic strings, just write out the contents of AutoCompleteCustomSource from the TextBox to a text file, on separate lines.
I had a few minutes, so I wrote up a complete code example...I would've before as I always try to show code, but didn't have time. Anyway, here's the whole thing (minus the designer code).
namespace AutoComplete
{
public partial class Main : Form
{
//so you don't have to address "txtMain.AutoCompleteCustomSource" every time
AutoCompleteStringCollection acsc;
public Main()
{
InitializeComponent();
//Set to use a Custom source
txtMain.AutoCompleteSource = AutoCompleteSource.CustomSource;
//Set to show drop down *and* append current suggestion to end
txtMain.AutoCompleteMode = AutoCompleteMode.SuggestAppend;
//Init string collection.
acsc = new AutoCompleteStringCollection();
//Set txtMain's AutoComplete Source to acsc
txtMain.AutoCompleteCustomSource = acsc;
}
private void txtMain_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
{
//Only keep 10 AutoComplete strings
if (acsc.Count < 10)
{
//Add to collection
acsc.Add(txtMain.Text);
}
else
{
//remove oldest
acsc.RemoveAt(0);
//Add to collection
acsc.Add(txtMain.Text);
}
}
}
private void Main_FormClosed(object sender, FormClosedEventArgs e)
{
//open stream to AutoComplete save file
StreamWriter sw = new StreamWriter("AutoComplete.acs");
//Write AutoCompleteStringCollection to stream
foreach (string s in acsc)
sw.WriteLine(s);
//Flush to file
sw.Flush();
//Clean up
sw.Close();
sw.Dispose();
}
private void Main_Load(object sender, EventArgs e)
{
//open stream to AutoComplete save file
StreamReader sr = new StreamReader("AutoComplete.acs");
//initial read
string line = sr.ReadLine();
//loop until end
while (line != null)
{
//add to AutoCompleteStringCollection
acsc.Add(line);
//read again
line = sr.ReadLine();
}
//Clean up
sr.Close();
sr.Dispose();
}
}
}
This code will work exactly as is, you just need to create the GUI with a TextBox named txtMain and hook up the KeyDown, Closed and Load events to the TextBox and Main form.
Also note that, for this example and to make it simple, I just chose to detect the Enter key being pressed as my trigger to save the string to the collection. There is probably more/different events that would be better, depending on your needs.
Also, the model used for populating the collection is not very "smart." It simply deletes the oldest string when the collection gets to the limit of 10. This is likely not ideal, but works for the example. You would probably want some sort of rating system (especially if you really want it to be Google-ish)
A final note, the suggestions will actually show up in the order they are in the collection. If for some reason you want them to show up differently, just sort the list however you like.
Hope that helps!
I store the completion list in the registry.
The code I use is below. It's reusable, in three steps:
replace the namespace and classname in this code with whatever you use.
Call the FillFormFromRegistry() on the Form's Load event, and call SaveFormToRegistry on the Closing event.
compile this into your project.
You need to decorate the assembly with two attributes: [assembly: AssemblyProduct("...")] and [assembly: AssemblyCompany("...")] . (These attributes are normally set automatically in projects created within Visual Studio, so I don't count this as a step.)
Managing state this way is totally automatic and transparent to the user.
You can use the same pattern to store any sort of state for your WPF or WinForms app. Like state of textboxes, checkboxes, dropdowns. Also you can store/restore the size of the window - really handy - the next time the user runs the app, it opens in the same place, and with the same size, as when they closed it. You can store the number of times an app has been run. Lots of possibilities.
namespace Ionic.ExampleCode
{
public partial class NameOfYourForm
{
private void SaveFormToRegistry()
{
if (AppCuKey != null)
{
// the completion list
var converted = _completions.ToList().ConvertAll(x => x.XmlEscapeIexcl());
string completionString = String.Join("¡", converted.ToArray());
AppCuKey.SetValue(_rvn_Completions, completionString);
}
}
private void FillFormFromRegistry()
{
if (!stateLoaded)
{
if (AppCuKey != null)
{
// get the MRU list of .... whatever
_completions = new System.Windows.Forms.AutoCompleteStringCollection();
string c = (string)AppCuKey.GetValue(_rvn_Completions, "");
if (!String.IsNullOrEmpty(c))
{
string[] items = c.Split('¡');
if (items != null && items.Length > 0)
{
//_completions.AddRange(items);
foreach (string item in items)
_completions.Add(item.XmlUnescapeIexcl());
}
}
// Can also store/retrieve items in the registry for
// - textbox contents
// - checkbox state
// - splitter state
// - and so on
//
stateLoaded = true;
}
}
}
private Microsoft.Win32.RegistryKey AppCuKey
{
get
{
if (_appCuKey == null)
{
_appCuKey = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(AppRegistryPath, true);
if (_appCuKey == null)
_appCuKey = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(AppRegistryPath);
}
return _appCuKey;
}
set { _appCuKey = null; }
}
private string _appRegistryPath;
private string AppRegistryPath
{
get
{
if (_appRegistryPath == null)
{
// Use a registry path that depends on the assembly attributes,
// that are presumed to be elsewhere. Example:
//
// [assembly: AssemblyCompany("Dino Chiesa")]
// [assembly: AssemblyProduct("XPathVisualizer")]
var a = System.Reflection.Assembly.GetExecutingAssembly();
object[] attr = a.GetCustomAttributes(typeof(System.Reflection.AssemblyProductAttribute), true);
var p = attr[0] as System.Reflection.AssemblyProductAttribute;
attr = a.GetCustomAttributes(typeof(System.Reflection.AssemblyCompanyAttribute), true);
var c = attr[0] as System.Reflection.AssemblyCompanyAttribute;
_appRegistryPath = String.Format("Software\\{0}\\{1}",
p.Product, c.Company);
}
return _appRegistryPath;
}
}
private Microsoft.Win32.RegistryKey _appCuKey;
private string _rvn_Completions = "Completions";
private readonly int _MaxMruListSize = 14;
private System.Windows.Forms.AutoCompleteStringCollection _completions;
private bool stateLoaded;
}
public static class Extensions
{
public static string XmlEscapeIexcl(this String s)
{
while (s.Contains("¡"))
{
s = s.Replace("¡", "¡");
}
return s;
}
public static string XmlUnescapeIexcl(this String s)
{
while (s.Contains("¡"))
{
s = s.Replace("¡", "¡");
}
return s;
}
public static List<String> ToList(this System.Windows.Forms.AutoCompleteStringCollection coll)
{
var list = new List<String>();
foreach (string item in coll)
{
list.Add(item);
}
return list;
}
}
}
Some people shy away from using the Registry for storing state, but I find it's really easy and convenient. If you like, You can very easily build an installer that removes all the registry keys on uninstall.

Categories

Resources