Introduction
A few weeks ago, I decided I wanted to create a screen saver that fades quotes on/off the screen. I've subsequenty lost interest in the project, but I learned some handy things in the process of becoming bored. This certainly isn't rocket science, but I've seen a few people ask about it, so I thought I'd share my findings.
Putting Text on a Bitmap
This is so ridiculously simple, it almost hurts to describe. Essentially, you only need five lines of code (with three optional lines shown for completeness and making the text "pretty".
Bitmap bitmap = new Bitmap(800, 600);
Graphics g = Graphics.FromImage(bitmap);
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
g.Clear(Color.Transparent);
TextRenderer.DrawText(g, "This is my text", myFont, new Point(0, 0), Color.Red);
m_bitmap = bitmap;
g.Dispose();
So there you have it. Simple eh? Yeah - I think so, too. However, if I stopped here, this article really wouldn't be very interesting, now would it? For that reason, I'm going to talk a little about the sample application provided with this article.
The sample app always runs maximized, and displays randomly selected quotes at random positions on the screen. This means we need to be cognizant of the width and height of the quote bitmap so that we can display it without any part of it being cut off by an edge of the screen. Some of the quotes are quite long, and some are quite short, so that means we have to dynamically calculate the size of the rectangle for each quote before it's displayed. On top of all that, the app suppports a width constraint as specified by the programmer (or user if a settings form is provided). For this app, it's hard-coded for the sake of example.
After a quote is selected, the app calls a method called GetConstrainedTextHeight
. This function accepts a font, the original text string, and a ref
parameter that will contain the modified version of the original text string. The method returns a rectangle object that is large enough to contain the rendered text. Just follow the coments to see what we're doing.
private Rectangle GetConstrainedTextHeight(Font font, string textToParse, ref string resultText)
{
int quoteAreaWidth = m_saverSettings.QuoteAreaWidth;
Bitmap bitmap = new Bitmap(100, quoteAreaWidth);
Graphics g = Graphics.FromImage(bitmap);
SizeF sz = g.MeasureString(textToParse, font);
if (sz.Width <= quoteAreaWidth)
{
resultText = textToParse;
g.Dispose();
bitmap.Dispose();
return new Rectangle(new Point(0, 0), new Size((int)sz.Width+5, (int)sz.Height));
}
resultText = "";
string[] words = textToParse.Trim().Split(' ');
string nextLine = "";
string word = "";
for (int i = 0; i < words.Length; i++)
{
word = words[i];
SizeF lineSize = g.MeasureString(nextLine, font);
SizeF wordSize = g.MeasureString(" " + word, font);
if (lineSize.Width + wordSize.Width < quoteAreaWidth)
{
nextLine = string.Format("{0} {1}", nextLine, word);
if (i == words.Length - 1)
{
resultText += nextLine;
}
}
else
{
resultText += (nextLine + "\n");
nextLine = word;
if (i == words.Length - 1)
{
resultText += nextLine;
}
}
}
sz = g.MeasureString(resultText, font);
g.Dispose();
bitmap.Dispose();
return new Rectangle(new Point(0, 0), new Size((int)sz.Width, (int)sz.Height));
}
We call the GetConstrainedTextHeight()
method twice - once for the quote text itself, and once for the author text. After all, there's really no point in leaving anything to chance. Besides, we need the author's rectangle to complete the positioning calculation. After getting the two rectangles, we can take care of positioning the bitmap on the screen. Remember, in the sample app it's always running in a maximized window state. For this reason, we simply use the screen resolution as the constraining rectangle.
string newQuoteText = "";
Rectangle quoteRect = GetConstrainedTextHeight(m_saverSettings.QuoteFont,
quoteItem.Text,
ref newQuoteText);
string newAuthorText = "";
Rectangle authorRect = GetConstrainedTextHeight(m_saverSettings.AuthorFont,
quoteItem.Author,
ref newAuthorText);
authorRect.X = quoteRect.Right - authorRect.Width;
authorRect.Y = quoteRect.Bottom;
Finally, some quality properties are set, and the text is actually rendered to the bitmap.
Bitmap bitmap = new Bitmap(quoteRect.Width, quoteRect.Height + authorRect.Height + 2);
Graphics g = Graphics.FromImage(bitmap);
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
g.Clear(Color.Transparent);
TextRenderer.DrawText(g,
newQuoteText,
m_saverSettings.QuoteFont,
new Point(quoteRect.X, quoteRect.Y),
m_saverSettings.QuoteColor);
TextRenderer.DrawText(g,
newAuthorText,
m_saverSettings.AuthorFont,
new Point(authorRect.X, authorRect.Y+2),
m_saverSettings.AuthorColor);
m_bitmap = bitmap;
g.Dispose();
Other Points of Interest
This article features a simple windows form application that I used to test the core functionality. Besides demonstrating the dynamic creation and display of a bitmap, this article illustrates a method for making your Windows form application go full screen, the use of a BackgroundWorker object that's used to do the real work, as well as some rudimentary use of LINQ when loading and saving XML files.
Full-screen Mode
At work, I needed to make an application consume the entire screen, including the task bar. This entailed changing the border style to "None", make the formthe top-most window, and finally, using up the entire screen. The only way I know of to *truly* go full-screen is to use the Interop features of .Net, so I found this class on the internet that does exactly what we need:
public class WinAPI
{
[DllImport("user32.dll", EntryPoint = "GetSystemMetrics")]
public static extern int GetSystemMetrics(int which);
[DllImport("user32.dll")]
public static extern void
SetWindowPos(IntPtr hwnd, IntPtr hwndInsertAfter,
int X, int Y, int width, int height, uint flags);
private const int SM_CXSCREEN = 0;
private const int SM_CYSCREEN = 1;
private static IntPtr HWND_TOP = IntPtr.Zero;
private const int SWP_SHOWWINDOW = 64;
public static int ScreenX
{
get { return GetSystemMetrics(SM_CXSCREEN);}
}
public static int ScreenY
{
get { return GetSystemMetrics(SM_CYSCREEN);}
}
public static void SetWinFullScreen(IntPtr hwnd)
{
SetWindowPos(hwnd, HWND_TOP, 0, 0, ScreenX, ScreenY, SWP_SHOWWINDOW);
}
}
To make all this happen, we write the following code:
WinAPI.SetWinFullScreen(this.Handle);
this.TopMost = true;
this.FormBorderStyle = FormBorderStyle.None;
Of course, this doesn't keep the user from alt-tabbing out of the app, but there's a highly-regarded article here on CodeProject that discusses hooking the keyboard using C#, and I sugggest that you start there if you want to address this issue.
The Basic Framework
We start out with a standard Windows Form application. Since we're only dealing with a single form with no controls, there's no point in setting the form properties in the using IDE. So, it's off to the form's constructor, on a manual-typing adventure (c'mon - I know you'd rather do it this way).
First up, we want to set some styles regarding painting within the form. For some reason, I could never get this to work completely when setting these properties in the IDE, so I moved them to the object constructor:
SetStyle(ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint |
ControlStyles.DoubleBuffer, true);
We also need to override the Paint method, so we can display the bitmap.
private void Form1_Paint(object sender, PaintEventArgs e)
{
if (m_bitmap != null)
{
try
{
e.Graphics.DrawImage(m_bitmap, m_rect, 0, 0, m_rect.Width, m_rect.Height, GraphicsUnit.Pixel, m_attributes);
}
catch (Exception ex)
{
if (ex != null) {}
}
}
}
Finally, we use a BackgroundWorker object to control the fading of the bitmaps.
private void fader_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker worker = sender as BackgroundWorker;
if (m_bitmap != null)
{
m_bitmap.Dispose();
}
m_bitmapType = BitmapType.Quote;
switch (m_saverSettings.QuoteOrder)
{
case OrderSelector.Random:
{
m_nextQuoteIndex = RandomQuoteIndex();
}
break;
default:
{
m_nextQuoteIndex = NextQuoteIndex();
}
break;
}
if (m_nextQuoteIndex < 0 || m_nextQuoteIndex > m_quotes.Count)
{
return;
}
PrepareQuoteImage(m_quotes[m_nextQuoteIndex]);
int x = m_random.Next(0, Math.Max(0, WinAPI.ScreenX - m_bitmap.Width));
int y = m_random.Next(0, Math.Max(0, WinAPI.ScreenY - m_bitmap.Height));
m_rect = new Rectangle(x, y, m_bitmap.Width, m_bitmap.Height);
float delta = 0.05f;
m_alpha = 0.0f;
while (m_alpha != 1.0f && !worker.CancellationPending)
{
m_colorMatrix.Matrix33 = m_alpha;
try
{
m_attributes.SetColorMatrix(m_colorMatrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);
}
catch (Exception ex)
{
if (ex != null) {}
}
m_alpha += delta;
m_alpha = Math.Min(1.0f, m_alpha);
this.Invalidate(m_rect);
Thread.Sleep(35);
}
int sleeper = 0;
int sleepTime = 100;
while (sleeper <= m_saverSettings.OnDuration && !worker.CancellationPending)
{
Thread.Sleep(sleepTime);
sleeper += sleepTime;
}
m_alpha = 1.0f;
while (m_alpha != 0.0f && !worker.CancellationPending)
{
m_alpha -= delta;
m_alpha = Math.Max(0.0f, m_alpha);
m_colorMatrix.Matrix33 = m_alpha;
m_attributes.SetColorMatrix(m_colorMatrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);
this.Invalidate(m_rect);
Thread.Sleep(35);
}
}
Loading The Data - LINQ to the Rescue!
I like patriotic quotes. Despite most of them being 200 years old, and some being of questionable origin, I like to read them. It makes me feel good to know I'm a part of what was an uncommon (and even unpopular) idea. I can understand that this kind of stuff isn't necessarily everyone's cup of tea, but since this is my article, I claim the right of creative license.
As far as quotes are concerned, Our needs are quite simple - we have the quote text itself, and the author of that quote. While in the process of writing this article, I had cause to start using LINQ at work, and decided it might be fun to add LINQ to this sample application. Initially, I was using a simple comma-delimited text file to store the quotes, and everything was right with the world. However, to really experience LINQ, I decided I would use an XML file instead because that's currently how most people load data that's not in a database. Here's the QuoteItem class:
public class QuoteItem
{
private string m_text;
private string m_author;
public string Text { get { return m_text; } }
public string Author { get { return m_author; } }
public XElement XmlElement
{
get
{
return new XElement("QUOTE",
new XElement("TEXT", Text),
new XElement("AUTHOR", Author));
}
}
public QuoteItem(string text, string author)
{
m_text = text;
m_author = author;
}
public QuoteItem (XElement element)
{
m_text = element.Element("TEXT").Value;
m_author = element.Element("AUTHOR").Value;
}
}
Notice that there's a property that returns the data as an XElement. This helps to keep the code outside this class a little cleaner. Toward that same end, there's a constructor overload that accepts an XElement object.
Having started with a .txt file, I first wanted to automate it's conversion into XML, but first, I had to load the text file. Since the code was already written to do that, I decided to simply modify it enough to cause a conversion to take place.
private void LoadQuotes()
{
m_quotes.Clear();
if (File.Exists(m_quotesFileName))
{
LoadQuotesFromXML();
}
if (m_quotes.Count == 0)
{
if (m_quotes.Count > 0)
{
SaveQuotesToXML();
m_quotes.Clear();
LoadQuotesFromXML();
}
}
First, I check for the exstance of the XML file, and load the data from it if it's there. If after loading the XML file, there are no quotes loaded, it tries to read the .txt version of the file. Finally, if we loaded some quotes, we save them to the XML file, clear the list, and repopulate it from the XML file. This allows us to verify that the XML file was indeed created.
Loading XML data is a piece of cake with LINQ. Our LoadQuotes() function required more than two-dozen lines of code to load the data from a comma-delimited text file, and this doesn't even count the code in the string parsing class. With LINQ, the line-count is reduced to just six lines of code:
private void LoadQuotesFromXML()
{
XDocument quotes = XDocument.Load(m_quotesFileName);
var dataSet = from data in quotes.Descendants("QUOTE") select data;
foreach (var quote in dataSet)
{
m_quotes.Add(new QuoteItem(quote));
}
}
Saving the data as XML is fairly straightforward using LINQ:
private void SaveQuotesToXML()
{
XDocument quotes = new XDocument(new XDeclaration("1.0", "utf-8", "yes"), new XComment("Quotes file"));
var root = new XElement("QUOTES", from q in m_quotes select q.XmlElement);
quotes.Add(root);
quotes.Save(m_quotesFileName);
}
I seem to remember it being a complete pain-in-the-ass to create XML and save it to a file before LINQ came along. Now, it's just four lines of code Remember when I said hiding details in black-box classes kept your code clean? This is a perfect example of what I mean.
Closing
The sample app serves no useful purpose beyond letting me test the code I was going to use in the screensaver I intended to write. On the way, I use a couple of techniques and classes that most of the rest of you can make use of. If you want to step through the code that creates the XML file, just delete the XML file from the program folder.
History
09/29/2008: Corrected some spelling errors, and modified some of the descriptive text.
09/28/2008: Posted original article.