Decorator Pattern Help - c#

I am working on an Oreilly's example
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
using System.Collections.Generic;
using Given;
// Decorator Pattern Example Judith Bishop August 2007
// Draws a single photograph in a window of fixed size
// Has decorators that are BorderedPhotos and TaggedPhotos that can be composed and added
// in different combinations
namespace Given {
// The original Photo class
public class Photo : Form {
Image image;
public Photo () {
image = new Bitmap("jug.jpg");
this.Text = "Lemonade";
this.Paint += new PaintEventHandler(Drawer);
}
public virtual void Drawer(Object source, PaintEventArgs e) {
e.Graphics.DrawImage(image,30,20);
}
}
}
class DecoratorPatternExample {
// This simple BorderedPhoto decorator adds a colored BorderedPhoto of fixed size
class BorderedPhoto : Photo {
Photo photo;
Color color;
public BorderedPhoto (Photo p, Color c) {
photo = p;
color=c;
}
public override void Drawer(Object source, PaintEventArgs e) {
photo.Drawer(source, e);
e.Graphics.DrawRectangle(new Pen(color, 10),25,15,215,225);
}
}
// The TaggedPhoto decorator keeps track of the tag number which gives it
// a specific place to be written
class TaggedPhoto : Photo {
Photo photo;
string tag;
int number;
static int count;
List <string> tags = new List <string> ();
public TaggedPhoto(Photo p, string t) {
photo = p;
tag = t;
tags.Add(t);
number = ++count;
}
public override void Drawer(Object source, PaintEventArgs e) {
photo.Drawer(source,e);
e.Graphics.DrawString(tag,
new Font("Arial", 16),
new SolidBrush(Color.Black),
new PointF(80,100+number*20));
}
public string ListTaggedPhotos() {
string s = "Tags are: ";
foreach (string t in tags) s +=t+" ";
return s;
}
}
static void Main () {
// Application.Run acts as a simple client
Photo photo;
TaggedPhoto foodTaggedPhoto, colorTaggedPhoto, tag;
BorderedPhoto composition;
// Compose a photo with two TaggedPhotos and a blue BorderedPhoto
photo = new Photo();
Application.Run(photo);
foodTaggedPhoto = new TaggedPhoto (photo,"Food");
colorTaggedPhoto = new TaggedPhoto (foodTaggedPhoto,"Yellow");
composition = new BorderedPhoto(colorTaggedPhoto, Color.Blue);
Application.Run(composition);
Console.WriteLine(colorTaggedPhoto.ListTaggedPhotos());
// Compose a photo with one TaggedPhoto and a yellow BorderedPhoto
photo = new Photo();
tag = new TaggedPhoto (photo,"Jug");
composition = new BorderedPhoto(tag, Color.Yellow);
Application.Run(composition);
Console.WriteLine(tag.ListTaggedPhotos());
}
}
/* Output
TaggedPhotos are: Food Yellow
TaggedPhotos are: Food Yellow Jug
*/
Next Exercise is
Assume that the Photo class was written with Drawer as a plain (not virtual)
method and it cannot be altered. Reconstruct Example 2-2 so that it works
under this constraint
How can i do that ?
My Approach
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Windows.Forms;
using GivenWihInterface;
namespace GivenWihInterface
{
interface IPhoto
{
void Drawer(object sender, PaintEventArgs e);
}
class Photo : Form, IPhoto
{
Image image;
public Photo()
{
image = new Bitmap(#"c:\users\anishmarokey\documents\visual studio 2010\Projects\Design_Pattern_Decorator\DecoratorPattern_RealExample\Images\apple-6.jpg");
this.Text = "Apple";
this.Paint += new PaintEventHandler(Drawer);
}
public void Drawer(object sender, PaintEventArgs e)
{
e.Graphics.DrawImage(image, 20, 20);
}
}
class BorderPhoto : Form, IPhoto
{
IPhoto pho;
Color color;
public BorderPhoto(IPhoto p, Color c)
{
pho = p;
color = c;
this.Paint += new PaintEventHandler(Drawer);
}
public void Drawer(object sender, PaintEventArgs e)
{
pho.Drawer(sender, e);
e.Graphics.DrawRectangle(new Pen(color, 10), 25, 15, 215, 225);
}
}
}
namespace DecoratorPattern_RealExample
{
class DecoratorPatternWithInterface
{
static void Dispaly(GivenWihInterface.IPhoto p)
{
Application.Run((Form)p);
}
static void Main()
{
IPhoto component = new GivenWihInterface.Photo();
Dispaly(component);
component = new GivenWihInterface.Photo();
IPhoto p = new GivenWihInterface.BorderPhoto(component,Color.Red);
Application.Run((Form)p);
}
}
}
is this the correct Way?

Yes, that is an appropriate "decorator" implementation. The only thing I'd question is whether you actually need to inherit from Form, or whether implementing IPhoto is sufficient. Which can only be answered with more context.
Also, the hard-coded (dimensions?) values look like there may be a better way if some existing values are available somewhere.
The example itself is unusual, though - you've had to introduce the interface, which is pretty much as much of a change as you are trying to avoid; and the type handles an event on itself, which is bad practice. I almost wonder if they want you to hook into the event pipeline, but that wouldn't really be a decorator.
I suspect they want you to code against Form rather than the IPhoto you have introduced, which would allow you to decorate lots of things. But you would need to have a known method on Form to call, for example Paint() - except that is an event here, not a method so the name will be different. And again we could hook the event, but that isn't classic decorator usage.

Related

Problems with opening a window - Forms

I'm trying to make a simple 2d game with C# using forms but when i run it at 60 fps it uses 50% of my CPU and doesn't use my GPU at all. I've been looking for hours and can't find another way to do it. How do i get it to run on my GPU?
Also I'm using a thread to wait in between frames with Thread.Sleep(15). I don't know if this is a good way to do it.
I was following this video for the window https://www.youtube.com/watch?v=JnGM1p2vsbE
This is my code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Windows.Forms;
using System.Drawing;
namespace RadiantEngine
{
class Canvas : Form
{
public Canvas()
{
this.DoubleBuffered = true;
}
}
public abstract class RadiantWindow
{
public Vector2 ScreenSize;
public string Title;
private Canvas Window;
private Thread GameLoopThread;
public float num;
Bitmap image = (Bitmap)Image.FromFile("F:/VS/RadiantEngine/RadiantEngine/dog.png");
public RadiantWindow(Vector2 ScreenSize, string Title)
{
this.ScreenSize = ScreenSize;
this.Title = Title;
EngineInit();
}
public RadiantWindow(float x, float y, string Title)
{
Vector2 _ScreenSize = new Vector2();
this.ScreenSize = _ScreenSize;
this.Title = Title;
EngineInit();
}
void EngineInit()
{
Window = new Canvas();
Window.Text = Title;
Window.Size = new Size((int)ScreenSize.x, (int)ScreenSize.y);
Window.Paint += Renderer;
GameLoopThread = new Thread(GameLoop);
GameLoopThread.Start();
Application.Run(Window);
}
private void Renderer(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.Clear(Color.Aqua);
g.DrawImage(RadiantEngine.engine.renderer.FrameRender(), 0, 0);
}
void GameLoop()
{
OnLoad();
while (GameLoopThread.IsAlive)
{
try
{
OnDraw();
Window.BeginInvoke((MethodInvoker)delegate { Window.Refresh(); });
}
catch
{
Console.WriteLine("Game Is Loading");
}
Thread.Sleep(15);
}
}
public abstract void OnDraw();
public abstract void OnLoad();
}
}
Thanks for any help.

Pass double from external class to form

Please Help, I'm a complete noob. However, i'm working with some complicated API.
I have external class:
namespace IB_CSharp_RealTime_WinForms_CS
{
//! [ewrapperimpl]
public class EWrapperImpl : EWrapper
{
...
//! [historicaldata]
public virtual void historicalData(int reqId, string date, double open, double high, double low, double close, int volume, int count, double WAP, bool hasGaps)
{
Console.WriteLine("HistoricalData. "+reqId+" - Date: "+date+", Open: "+open+", High: "+high+", Low: "+low+", Close: "+close+", Volume: "+volume+", Count: "+count+", WAP: "+WAP+", HasGaps: "+hasGaps);
}
...
And I have a Form
namespace IB_CSharp_RealTime_WinForms_CS
{
public partial class Form1 : Form
{
...
How can I pass "double high" from my external class to my Form?
Thank you in advance!
My full code:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading; // Add this for multi-threading support
using IBApi; // Add this for IB API Support
namespace IB_CSharp_RealTime_WinForms_CS
{
public partial class Form1 : Form
{
// This delegate enables asynchronous calls for setting
// the text property on a ListBox control.
delegate void SetTextCallback(string text);
public void AddListBoxItem(string text)
{
// See if a new invocation is required form a different thread
if (this.lbData.InvokeRequired)
{
SetTextCallback d = new SetTextCallback(AddListBoxItem);
this.Invoke(d, new object[] { text });
}
else
{
// Add the text string to the list box
this.lbData.Items.Add(text);
}
}
// Create the ibClient object to represent the connection
// This will be used throughout the form
IB_CSharp_RealTime_WinForms_CS.EWrapperImpl ibClient;
private string tickerId;
public Form1()
{
InitializeComponent();
// Instantiate the ibClient
ibClient = new IB_CSharp_RealTime_WinForms_CS.EWrapperImpl();
}
private void Form1_Load(object sender, EventArgs e)
{
}
private void btnConnect_Click(object sender, EventArgs e)
{
ibClient.ClientSocket.eConnect("", 7496, 0);
var reader = new EReader(ibClient.ClientSocket, ibClient.Signal);
reader.Start();
new Thread(() => {
while (ibClient.ClientSocket.IsConnected())
{
ibClient.Signal.waitForSignal();
reader.processMsgs();
}
})
{ IsBackground = true }.Start();
while (ibClient.NextOrderId <= 0) { }
// Set up the form object in the EWrapper
ibClient.myform = (Form1)Application.OpenForms[0];
}
private void btnDisconnect_Click(object sender, EventArgs e)
{
// Disconnect from interactive Brokers
ibClient.ClientSocket.eDisconnect();
}
private void btnStart_Click(object sender, EventArgs e)
{
// Create a new contract to specify the security we are searching for
IBApi.Contract contract = new IBApi.Contract();
// Create a new TagValueList object (for API version 9.71 and later)
List < IBApi.TagValue > mktDataOptions = new List<IBApi.TagValue>();
// Set the underlying stock symbol from the tbSymbol text box
contract.Symbol = tbSymbol.Text;
// Set the Security type to STK for a Stock
contract.SecType = "STK";
// Use "SMART" as the general exchange
contract.Exchange = "SMART";
// Set the primary exchange (sometimes called Listing exchange)
// Use either NYSE or ISLAND
contract.PrimaryExch = "ISLAND";
// Set the currency to USD
contract.Currency = "USD";
// Kick off the subscription for real-time data (add the mktDataOptions list for API v9.71)
ibClient.ClientSocket.reqMktData(1, contract, "", false, mktDataOptions);
// For API v9.72 and higher, add one more parameter for regulatory snapshot
// ibClient.ClientSocket.reqMktData(1, contract, "", false, false, mktDataOptions);
} // end btnStart_Click
private void btnStop_Click(object sender, EventArgs e)
{
// Make the call to cancel the market data subscription
ibClient.ClientSocket.cancelMktData(1);
}
}
}
What I want is TextBox1.Text to show the value of "high". Basically if I can learn on how to pass that value I can use it to pass all other values.

Draw Line & Thickness in a Class: method group / OverLoad issue

I'm trying to set up the code for a Line class with it's own thickness besides the DefaultLineThickness I have for my squares and circles. As much as I would like to use g.DrawLine(Pens.Black, 25, 40, 126, 126); it needs to be in it's own class and inherited which I've done with the other shapes. I found this topic closest to my issue but its in XAML and anywhere else it's just simple g.DrawLine
http://www.c-sharpcorner.com/uploadfile/mahesh/line-in-wpf/
Trying to make a crude bike out of circles, squares, and lines.
*edit
After enough times of playing around I'm on the brink of my goal. My only problem is that after I plug everything in and set the line thickness to x-number, it's giving me an error that it can't assign the number because of a 'method group'
Form1 Code
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace Bicycle
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
LineOne l1 = new LineOne(new PointF(50, 40));
l1.setFilledState(false);
l1.setLineColor(Color.Black);
l1.setLineThickness = (6);
//cannot assign to 'setLineThickness' because it is a method group'
l1.Draw(g);
sRectangle r2 = new sRectangle(new PointF(151, 160));
r2.setFilledState(true);
r2.setLineColor(Color.Green);
r2.setFilledColor(Color.Honeydew);
r2.Draw(g);
sRectangleEmpty r1 = new sRectangleEmpty(new PointF(150, 150));
r1.setFilledState(false);
r1.setLineColor(Color.Blue);
r1.Draw(g);
sCircle c1 = new sCircle(new PointF(180, 130));
c1.setFilledState(true);
c1.setLineColor(Color.Orange);
c1.setFilledColor(Color.Ivory);
c1.Draw(g);
sCircleEmpty c2 = new sCircleEmpty(new PointF(120, 130));
c2.setFilledState(false);
c2.setLineColor(Color.Black);
c2.Draw(g);
}
private void Form1_Load(object sender, EventArgs e)
{
}
}
}
The Line Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
namespace Bicycle
{
class LineOne : sRectangle
{
private void setDrawingAttributes()
{
const int lenght = 10;
Pen SmallPen = new Pen(Brushes.DeepSkyBlue);
SmallPen.LineJoin = System.Drawing.Drawing2D.LineJoin.Bevel;
PointF p1 = PointF.Add(location, new Size(-lenght / 2, 0));
}
private void init()
{
setDrawingAttributes();
}
public LineOne()
{
init();
}
public LineOne(PointF p)
: base(p)
{
init();
}
public override void Draw(Graphics g)
{
g.DrawEllipse(pen, rect);
}
}
}
The Shape Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
namespace Bicycle
{
class Shape
{
private Color DefaultLineColor = Color.Black;
private Color DefaultFillColor = Color.Blue;
private float DefaultLineThickness = 2;
protected bool bOutLine;
protected bool bFilled;
protected Pen pen;
protected Brush brush;
protected PointF location;
private void setDrawingAttributes()
{
pen = new Pen(DefaultLineColor, DefaultLineThickness);
brush = new SolidBrush(DefaultFillColor);
}
private void init()
{
bOutLine = true;
setDrawingAttributes();
}
public Shape()
{
init();
}
public Shape(PointF p)
{
location = p;
init();
}
public Color getFillColor()
{
return (DefaultFillColor);
}
public bool getFilledState()
{
return (bFilled);
}
public Color getLineColor()
{
return (DefaultLineColor);
}
public float getLineThickness()
{
return (DefaultLineThickness);
}
public bool getOutLineState()
{
return (bOutLine);
}
public bool isOutLine()
{
return (bOutLine);
}
public bool isFilled()
{
return (bFilled);
}
public void setFilledColor(Color C)
{
DefaultFillColor = C;
setDrawingAttributes();
}
public void setLineColor(Color C)
{
DefaultLineColor = C;
setDrawingAttributes();
}
public void setLineThickness(float value)
{
DefaultLineThickness = value;
setDrawingAttributes();
}
public void setFilledState(bool value)
{
bFilled = value;
}
public void setOutLineState(bool value)
{
bOutLine = value;
}
}
}
setLineThickness is a method, not a property.
Change l1.setLineThickness = (6); to l1.setLineThickness(6); (i.e. remove the equals sign)
Found it, after reviewing a article in regards of Overloads my Line code should had been
public override void Draw(Graphics g)
{
g.DrawLine(pen, new Point(0, 0), new Point(120, 95));
}
I'm now getting different strokes of lines when I play with the l1.setLineThickness(1); in my form code.
Thank you for your assist Mr.Williams on the clarification about LineThickness.

different print results from exactly the same code

I have two programs, one is a windows service and the other a windows forms application. They have exactly the same code for printing one A4 page. They both print to the same network printer and they start drawing at position 0,0.
private void pd_PrintCustomsDocument(object sender, PrintPageEventArgs ev)
{
Graphics g = ev.Graphics;
g.PageUnit = GraphicsUnit.Millimeter;
using (Font courierBig = new Font("Courier", 15))
{
g.DrawString("Shipping Invoice", courierBig, Brushes.Black, new Point(0, 0));
// etc
}
}
The windows forms app prints the document correctly, a page margin is used. But the service starts printing exactly at the edge of the paper.
Is there a difference between printing with gdi+ from a service and a windows forms application?
The code for the actual printing is divided into a base and subclass for overriding default printer settings like selecting page from a different tray:
public class PrintBehaviour : IDisposable
{
private string mPrinterName;
private PrintPageEventHandler mHandler;
private PrintDocument mDocument = new PrintDocument();
public PrintBehaviour(string name, PrintPageEventHandler handler)
{
mPrinterName = name;
mHandler = handler;
mDocument.PrintController = new StandardPrintController();
}
public virtual void SettingsOverride(PrintDocument doc) {}
public void Print()
{
SettingsOverride(mDocument);
mDocument.PrinterSettings.PrinterName = mPrinterName;
mDocument.PrintPage += new PrintPageEventHandler(mHandler);
mDocument.Print();
}
public void Dispose()
{
mDocument.Dispose();
}
}
public sealed class CustomsPrintBehaviour : PrintBehaviour
{
private string mPaperTray;
public CustomsPrintBehaviour(string name, PrintPageEventHandler handler, string paperTray)
: base(name, handler)
{
mPaperTray = paperTray;
}
public override void SettingsOverride(PrintDocument doc)
{
base.SettingsOverride(doc);
doc.DefaultPageSettings.Landscape = true;
foreach (PaperSource source in doc.PrinterSettings.PaperSources)
{
if (source.SourceName.Trim().ToUpper() == mPaperTray)
{
doc.DefaultPageSettings.PaperSource = source;
PaperSize size = new PaperSize { RawKind = (int)PaperKind.A4 };
doc.DefaultPageSettings.PaperSize = size;
break;
}
}
}
}
and called like this:
using (var pb = new CustomsPrintBehaviour(_customsPrinter, pd_PrintCustomsDocument, kv["PaperTray"].ToUpper()))
{
pb.Print();
}
From MSDN:
GDI+ functions and classes are not supported for use within a
Windows service. Attempting to use these functions and classes from a
Windows service may produce unexpected problems, such as diminished
service performance and run-time exceptions or errors.

Elegant Log Window in WinForms C#

I am looking for ideas on an efficient way to implement a log window for a windows forms application. In the past I have implemented several using TextBox and RichTextBox but I am still not totally satisfied with the functionality.
This log is intended to provide the user with a recent history of various events, primarily used in data-gathering applications where one might be curious how a particular transaction completed. In this case, the log need not be permanent nor saved to a file.
First, some proposed requirements:
Efficient and fast; if hundreds of lines are written to the log in quick succession, it needs to consume minimal resources and time.
Be able to offer a variable scrollback of up to 2000 lines or so. Anything longer is unnecessary.
Highlighting and color are preferred. Font effects not required.
Automatically trim lines as the scrollback limit is reached.
Automatically scroll as new data is added.
Bonus but not required: Pause auto-scrolling during manual interaction such as if the user is browsing the history.
What I have been using so far to write and trim the log:
I use the following code (which I call from other threads):
// rtbLog is a RichTextBox
// _MaxLines is an int
public void AppendLog(string s, Color c, bool bNewLine)
{
if (rtbLog.InvokeRequired)
{
object[] args = { s, c, bNewLine };
rtbLog.Invoke(new AppendLogDel(AppendLog), args);
return;
}
try
{
rtbLog.SelectionColor = c;
rtbLog.AppendText(s);
if (bNewLine) rtbLog.AppendText(Environment.NewLine);
TrimLog();
rtbLog.SelectionStart = rtbLog.TextLength;
rtbLog.ScrollToCaret();
rtbLog.Update();
}
catch (Exception exc)
{
// exception handling
}
}
private void TrimLog()
{
try
{
// Extra lines as buffer to save time
if (rtbLog.Lines.Length < _MaxLines + 10)
{
return;
}
else
{
string[] sTemp = rtxtLog.Lines;
string[] sNew= new string[_MaxLines];
int iLineOffset = sTemp.Length - _MaxLines;
for (int n = 0; n < _MaxLines; n++)
{
sNew[n] = sTemp[iLineOffset];
iLineOffset++;
}
rtbLog.Lines = sNew;
}
}
catch (Exception exc)
{
// exception handling
}
}
The problem with this approach is that whenever TrimLog is called, I lose color formatting. With a regular TextBox this works just fine (with a bit of modification of course).
Searches for a solution to this have never been really satisfactory. Some suggest to trim the excess by character count instead of line count in a RichTextBox. I've also seen ListBoxes used, but haven't successfully tried it.
I recommend that you don't use a control as your log at all. Instead write a log collection class that has the properties you desire (not including the display properties).
Then write the little bit of code that is needed to dump that collection to a variety of user interface elements. Personally, I would put SendToEditControl and SendToListBox methods into my logging object. I would probably add filtering capabilities to these methods.
You can update the UI log only as often as it makes sense, giving you the best possible performance, and more importantly, letting you reduce the UI overhead when the log is changing rapidly.
The important thing is not to tie your logging to a piece of UI, that's a mistake. Someday you may want to run headless.
In the long run, a good UI for a logger is probably a custom control. But in the short run, you just want to disconnect your logging from any specific piece of UI.
Here is something I threw together based on a much more sophisticated logger I wrote a while ago.
This will support color in the list box based on log level, supports Ctrl+V and Right-Click for copying as RTF, and handles logging to the ListBox from other threads.
You can override the number of lines retained in the ListBox (2000 by default) as well as the message format using one of the constructor overloads.
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Threading;
using System.Text;
namespace StackOverflow
{
public partial class Main : Form
{
public static ListBoxLog listBoxLog;
public Main()
{
InitializeComponent();
listBoxLog = new ListBoxLog(listBox1);
Thread thread = new Thread(LogStuffThread);
thread.IsBackground = true;
thread.Start();
}
private void LogStuffThread()
{
int number = 0;
while (true)
{
listBoxLog.Log(Level.Info, "A info level message from thread # {0,0000}", number++);
Thread.Sleep(2000);
}
}
private void button1_Click(object sender, EventArgs e)
{
listBoxLog.Log(Level.Debug, "A debug level message");
}
private void button2_Click(object sender, EventArgs e)
{
listBoxLog.Log(Level.Verbose, "A verbose level message");
}
private void button3_Click(object sender, EventArgs e)
{
listBoxLog.Log(Level.Info, "A info level message");
}
private void button4_Click(object sender, EventArgs e)
{
listBoxLog.Log(Level.Warning, "A warning level message");
}
private void button5_Click(object sender, EventArgs e)
{
listBoxLog.Log(Level.Error, "A error level message");
}
private void button6_Click(object sender, EventArgs e)
{
listBoxLog.Log(Level.Critical, "A critical level message");
}
private void button7_Click(object sender, EventArgs e)
{
listBoxLog.Paused = !listBoxLog.Paused;
}
}
public enum Level : int
{
Critical = 0,
Error = 1,
Warning = 2,
Info = 3,
Verbose = 4,
Debug = 5
};
public sealed class ListBoxLog : IDisposable
{
private const string DEFAULT_MESSAGE_FORMAT = "{0} [{5}] : {8}";
private const int DEFAULT_MAX_LINES_IN_LISTBOX = 2000;
private bool _disposed;
private ListBox _listBox;
private string _messageFormat;
private int _maxEntriesInListBox;
private bool _canAdd;
private bool _paused;
private void OnHandleCreated(object sender, EventArgs e)
{
_canAdd = true;
}
private void OnHandleDestroyed(object sender, EventArgs e)
{
_canAdd = false;
}
private void DrawItemHandler(object sender, DrawItemEventArgs e)
{
if (e.Index >= 0)
{
e.DrawBackground();
e.DrawFocusRectangle();
LogEvent logEvent = ((ListBox)sender).Items[e.Index] as LogEvent;
// SafeGuard against wrong configuration of list box
if (logEvent == null)
{
logEvent = new LogEvent(Level.Critical, ((ListBox)sender).Items[e.Index].ToString());
}
Color color;
switch (logEvent.Level)
{
case Level.Critical:
color = Color.White;
break;
case Level.Error:
color = Color.Red;
break;
case Level.Warning:
color = Color.Goldenrod;
break;
case Level.Info:
color = Color.Green;
break;
case Level.Verbose:
color = Color.Blue;
break;
default:
color = Color.Black;
break;
}
if (logEvent.Level == Level.Critical)
{
e.Graphics.FillRectangle(new SolidBrush(Color.Red), e.Bounds);
}
e.Graphics.DrawString(FormatALogEventMessage(logEvent, _messageFormat), new Font("Lucida Console", 8.25f, FontStyle.Regular), new SolidBrush(color), e.Bounds);
}
}
private void KeyDownHandler(object sender, KeyEventArgs e)
{
if ((e.Modifiers == Keys.Control) && (e.KeyCode == Keys.C))
{
CopyToClipboard();
}
}
private void CopyMenuOnClickHandler(object sender, EventArgs e)
{
CopyToClipboard();
}
private void CopyMenuPopupHandler(object sender, EventArgs e)
{
ContextMenu menu = sender as ContextMenu;
if (menu != null)
{
menu.MenuItems[0].Enabled = (_listBox.SelectedItems.Count > 0);
}
}
private class LogEvent
{
public LogEvent(Level level, string message)
{
EventTime = DateTime.Now;
Level = level;
Message = message;
}
public readonly DateTime EventTime;
public readonly Level Level;
public readonly string Message;
}
private void WriteEvent(LogEvent logEvent)
{
if ((logEvent != null) && (_canAdd))
{
_listBox.BeginInvoke(new AddALogEntryDelegate(AddALogEntry), logEvent);
}
}
private delegate void AddALogEntryDelegate(object item);
private void AddALogEntry(object item)
{
_listBox.Items.Add(item);
if (_listBox.Items.Count > _maxEntriesInListBox)
{
_listBox.Items.RemoveAt(0);
}
if (!_paused) _listBox.TopIndex = _listBox.Items.Count - 1;
}
private string LevelName(Level level)
{
switch (level)
{
case Level.Critical: return "Critical";
case Level.Error: return "Error";
case Level.Warning: return "Warning";
case Level.Info: return "Info";
case Level.Verbose: return "Verbose";
case Level.Debug: return "Debug";
default: return string.Format("<value={0}>", (int)level);
}
}
private string FormatALogEventMessage(LogEvent logEvent, string messageFormat)
{
string message = logEvent.Message;
if (message == null) { message = "<NULL>"; }
return string.Format(messageFormat,
/* {0} */ logEvent.EventTime.ToString("yyyy-MM-dd HH:mm:ss.fff"),
/* {1} */ logEvent.EventTime.ToString("yyyy-MM-dd HH:mm:ss"),
/* {2} */ logEvent.EventTime.ToString("yyyy-MM-dd"),
/* {3} */ logEvent.EventTime.ToString("HH:mm:ss.fff"),
/* {4} */ logEvent.EventTime.ToString("HH:mm:ss"),
/* {5} */ LevelName(logEvent.Level)[0],
/* {6} */ LevelName(logEvent.Level),
/* {7} */ (int)logEvent.Level,
/* {8} */ message);
}
private void CopyToClipboard()
{
if (_listBox.SelectedItems.Count > 0)
{
StringBuilder selectedItemsAsRTFText = new StringBuilder();
selectedItemsAsRTFText.AppendLine(#"{\rtf1\ansi\deff0{\fonttbl{\f0\fcharset0 Courier;}}");
selectedItemsAsRTFText.AppendLine(#"{\colortbl;\red255\green255\blue255;\red255\green0\blue0;\red218\green165\blue32;\red0\green128\blue0;\red0\green0\blue255;\red0\green0\blue0}");
foreach (LogEvent logEvent in _listBox.SelectedItems)
{
selectedItemsAsRTFText.AppendFormat(#"{{\f0\fs16\chshdng0\chcbpat{0}\cb{0}\cf{1} ", (logEvent.Level == Level.Critical) ? 2 : 1, (logEvent.Level == Level.Critical) ? 1 : ((int)logEvent.Level > 5) ? 6 : ((int)logEvent.Level) + 1);
selectedItemsAsRTFText.Append(FormatALogEventMessage(logEvent, _messageFormat));
selectedItemsAsRTFText.AppendLine(#"\par}");
}
selectedItemsAsRTFText.AppendLine(#"}");
System.Diagnostics.Debug.WriteLine(selectedItemsAsRTFText.ToString());
Clipboard.SetData(DataFormats.Rtf, selectedItemsAsRTFText.ToString());
}
}
public ListBoxLog(ListBox listBox) : this(listBox, DEFAULT_MESSAGE_FORMAT, DEFAULT_MAX_LINES_IN_LISTBOX) { }
public ListBoxLog(ListBox listBox, string messageFormat) : this(listBox, messageFormat, DEFAULT_MAX_LINES_IN_LISTBOX) { }
public ListBoxLog(ListBox listBox, string messageFormat, int maxLinesInListbox)
{
_disposed = false;
_listBox = listBox;
_messageFormat = messageFormat;
_maxEntriesInListBox = maxLinesInListbox;
_paused = false;
_canAdd = listBox.IsHandleCreated;
_listBox.SelectionMode = SelectionMode.MultiExtended;
_listBox.HandleCreated += OnHandleCreated;
_listBox.HandleDestroyed += OnHandleDestroyed;
_listBox.DrawItem += DrawItemHandler;
_listBox.KeyDown += KeyDownHandler;
MenuItem[] menuItems = new MenuItem[] { new MenuItem("Copy", new EventHandler(CopyMenuOnClickHandler)) };
_listBox.ContextMenu = new ContextMenu(menuItems);
_listBox.ContextMenu.Popup += new EventHandler(CopyMenuPopupHandler);
_listBox.DrawMode = DrawMode.OwnerDrawFixed;
}
public void Log(string message) { Log(Level.Debug, message); }
public void Log(string format, params object[] args) { Log(Level.Debug, (format == null) ? null : string.Format(format, args)); }
public void Log(Level level, string format, params object[] args) { Log(level, (format == null) ? null : string.Format(format, args)); }
public void Log(Level level, string message)
{
WriteEvent(new LogEvent(level, message));
}
public bool Paused
{
get { return _paused; }
set { _paused = value; }
}
~ListBoxLog()
{
if (!_disposed)
{
Dispose(false);
_disposed = true;
}
}
public void Dispose()
{
if (!_disposed)
{
Dispose(true);
GC.SuppressFinalize(this);
_disposed = true;
}
}
private void Dispose(bool disposing)
{
if (_listBox != null)
{
_canAdd = false;
_listBox.HandleCreated -= OnHandleCreated;
_listBox.HandleCreated -= OnHandleDestroyed;
_listBox.DrawItem -= DrawItemHandler;
_listBox.KeyDown -= KeyDownHandler;
_listBox.ContextMenu.MenuItems.Clear();
_listBox.ContextMenu.Popup -= CopyMenuPopupHandler;
_listBox.ContextMenu = null;
_listBox.Items.Clear();
_listBox.DrawMode = DrawMode.Normal;
_listBox = null;
}
}
}
}
I'll store this here as a help to Future Me when I want to use a RichTextBox for logging colored lines again. The following code removes the first line in a RichTextBox:
if ( logTextBox.Lines.Length > MAX_LINES )
{
logTextBox.Select(0, logTextBox.Text.IndexOf('\n')+1);
logTextBox.SelectedRtf = "{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1053\\uc1 }";
}
It took me way too long to figure out that setting SelectedRtf to just "" didn't work, but that setting it to "proper" RTF with no text content is ok.
My solution to creating a basic log window was exactly as John Knoeller suggested in his answer. Avoid storing log information directly in a TextBox or RichTextBox control, but instead create a logging class which can be used to populate a control, or write to a file, etc.
There are a few pieces to this example solution:
The logging class itself, Logger.
Modification of the RichTextBox control to add scroll-to-bottom functionality after an update; ScrollingRichTextBox.
The main form to demonstrate its use, LoggerExample.
First, the logging class:
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
namespace Logger
{
/// <summary>
/// A circular buffer style logging class which stores N items for display in a Rich Text Box.
/// </summary>
public class Logger
{
private readonly Queue<LogEntry> _log;
private uint _entryNumber;
private readonly uint _maxEntries;
private readonly object _logLock = new object();
private readonly Color _defaultColor = Color.White;
private class LogEntry
{
public uint EntryId;
public DateTime EntryTimeStamp;
public string EntryText;
public Color EntryColor;
}
private struct ColorTableItem
{
public uint Index;
public string RichColor;
}
/// <summary>
/// Create an instance of the Logger class which stores <paramref name="maximumEntries"/> log entries.
/// </summary>
public Logger(uint maximumEntries)
{
_log = new Queue<LogEntry>();
_maxEntries = maximumEntries;
}
/// <summary>
/// Retrieve the contents of the log as rich text, suitable for populating a <see cref="System.Windows.Forms.RichTextBox.Rtf"/> property.
/// </summary>
/// <param name="includeEntryNumbers">Option to prepend line numbers to each entry.</param>
public string GetLogAsRichText(bool includeEntryNumbers)
{
lock (_logLock)
{
var sb = new StringBuilder();
var uniqueColors = BuildRichTextColorTable();
sb.AppendLine($#"{{\rtf1{{\colortbl;{ string.Join("", uniqueColors.Select(d => d.Value.RichColor)) }}}");
foreach (var entry in _log)
{
if (includeEntryNumbers)
sb.Append($"\\cf1 { entry.EntryId }. ");
sb.Append($"\\cf1 { entry.EntryTimeStamp.ToShortDateString() } { entry.EntryTimeStamp.ToShortTimeString() }: ");
var richColor = $"\\cf{ uniqueColors[entry.EntryColor].Index + 1 }";
sb.Append($"{ richColor } { entry.EntryText }\\par").AppendLine();
}
return sb.ToString();
}
}
/// <summary>
/// Adds <paramref name="text"/> as a log entry.
/// </summary>
public void AddToLog(string text)
{
AddToLog(text, _defaultColor);
}
/// <summary>
/// Adds <paramref name="text"/> as a log entry, and specifies a color to display it in.
/// </summary>
public void AddToLog(string text, Color entryColor)
{
lock (_logLock)
{
if (_entryNumber >= uint.MaxValue)
_entryNumber = 0;
_entryNumber++;
var logEntry = new LogEntry { EntryId = _entryNumber, EntryTimeStamp = DateTime.Now, EntryText = text, EntryColor = entryColor };
_log.Enqueue(logEntry);
while (_log.Count > _maxEntries)
_log.Dequeue();
}
}
/// <summary>
/// Clears the entire log.
/// </summary>
public void Clear()
{
lock (_logLock)
{
_log.Clear();
}
}
private Dictionary<Color, ColorTableItem> BuildRichTextColorTable()
{
var uniqueColors = new Dictionary<Color, ColorTableItem>();
var index = 0u;
uniqueColors.Add(_defaultColor, new ColorTableItem() { Index = index++, RichColor = ColorToRichColorString(_defaultColor) });
foreach (var c in _log.Select(l => l.EntryColor).Distinct().Where(c => c != _defaultColor))
uniqueColors.Add(c, new ColorTableItem() { Index = index++, RichColor = ColorToRichColorString(c) });
return uniqueColors;
}
private string ColorToRichColorString(Color c)
{
return $"\\red{c.R}\\green{c.G}\\blue{c.B};";
}
}
}
The Logger class incorporates another class LogEntry which keeps track of the line number, timestamp, and desired color. A struct is used to build a Rich Text color table.
Next, here is the modified RichTextBox:
using System;
using System.Runtime.InteropServices;
namespace Logger
{
public class ScrollingRichTextBox : System.Windows.Forms.RichTextBox
{
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage(
IntPtr hWnd,
uint Msg,
IntPtr wParam,
IntPtr LParam);
private const int _WM_VSCROLL = 277;
private const int _SB_BOTTOM = 7;
/// <summary>
/// Scrolls to the bottom of the RichTextBox.
/// </summary>
public void ScrollToBottom()
{
SendMessage(Handle, _WM_VSCROLL, new IntPtr(_SB_BOTTOM), new IntPtr(0));
}
}
}
All I am doing here is inheriting a RichTextBox and adding a "scroll to bottom" method. There are various other questions about how to do this on StackOverflow, from which I derived this approach.
Finally, an example of using this class from a form:
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
namespace Logger
{
public partial class LoggerExample : Form
{
private Logger _log = new Logger(100u);
private List<Color> _randomColors = new List<Color> { Color.Red, Color.SkyBlue, Color.Green };
private Random _r = new Random((int)DateTime.Now.Ticks);
public LoggerExample()
{
InitializeComponent();
}
private void timerGenerateText_Tick(object sender, EventArgs e)
{
if (_r.Next(10) > 5)
_log.AddToLog("Some event to log.", _randomColors[_r.Next(3)]);
}
private void timeUpdateLogWindow_Tick(object sender, EventArgs e)
{
richTextBox1.Rtf = _log.GetLogAsRichText(true);
richTextBox1.ScrollToBottom();
}
}
}
This form is created with two timers, one to generate log entries pseudo-randomly, and one to populate the RichTextBox itself. In this example, the log class is instantiated with 100 lines of scroll-back. The RichTextBox control colors are set to have a black background with white and various color foregrounds. The timer to generate text is at a 100ms interval while the one to update the log window is at 1000ms.
Sample output:
It is far from perfect or finished, but here are some caveats and things that could be added or improved (some of which I have done in later projects):
With large values for maximumEntries, performance is poor. This logging class was only designed for a few hundred lines of scroll-back.
Replacing the text of the RichTextBox can result in flickering. I always keep the refresh timer at a relatively slow interval. (One second in this example.)
Adding to #2 above, some of my projects check if the log has any new entries before redrawing the RichTextBox content, to avoid unnecessarily refreshing it.
The timestamp on each log entry can be made optional and allow different formats.
There is no way to pause the log in this example, but many of my projects do provide a mechanism for pausing the scrolling behavior, to allow users to manually scroll, select, and copy text from the log window.
Feel free to modify and improve upon this example. Feedback is welcome.
I would say ListView is perfect for this (in Detail viewing mode), and its exactly what I use it for in a few internal apps.
Helpful tip: use BeginUpdate() and EndUpdate() if you know you will be adding/removing a lot of items at once.
I recently implemented something similar. Our approach was to keep a ring buffer of the scrollback records and just paint the log text manually (with Graphics.DrawString). Then if the user wants to scroll back, copy text, etc., we have a "Pause" button that flips back to a normal TextBox control.
If you want highlighting and color formatting, I'd suggest a RichTextBox.
If you want the auto scrolling, then use the ListBox.
In either case bind it to a circular buffer of lines.

Categories

Resources