You've seen font-selection toolbars in many Windows programs. These toolbars let you select a font, the font size, and attributes such as bold, italic, or underline. A font toolbar is a great feature for any application that uses fonts. In this article, we'll demonstrate how to set up a font toolbar. In particular, we'll show you how to use the API function EnumFontFamilesEx() to fill a combo box with a list of available fonts.
A font selection combo box
| A font point-size combo box
| A bold button
| An italic button
| An underline button
| |
The font size selection combo box is pretty basic: You simply load it with the font sizes you'd like to support. Although you'll see all sorts of point sizes listed in different Windows applications, showing the even numbers from 8 to 40 seems to be a sensible approach. This combo box should have the csDropDown style so users can enter any font size in addition to selecting a size from the list. It should also include code that changes the font size when the user presses [Enter].
The font attribute buttons (bold, italic, and underline) should be shown in either an up state or a down state. For example, if the bold style is in effect, then the bold button should appear in the down state. The buttons should operate independently, so that any font attribute can be on or off.
The next step is to add the buttons for selecting font attributes. Place three SpeedButton components on the panel to the right of the font size combo box. Change the captions as required, or create bitmaps for the buttons. In order for the buttons to toggle, you must set their AllowAllUp property to true. Then, set each button's GroupIndex property to a unique value: 1 for the bold button, 2 for the italic button, and 3 for the underline button, for example. Your form should now resemble the one shown in Figure A. Now you can move on to writing the code that will make the font toolbar useful.
Figure A: The font toolbar looks like this up to this point.
LOGFONT lf; lf.lfCharSet = DEFAULT_CHARSET; strcpy(lf.lfFaceName, ""); lf.lfPitchAndFamily = 0;The lfCharSet member controls the character set whose fonts will be enumerated. In this case, you want all fonts in the default Windows character set. Next, you set the lfFaceName member to an empty string, telling Windows that you want a list of all fonts in the specified character set. If you set lfFaceName to a specific typeface name (Arial, for example) then only the fonts in that font family will be enumerated. Finally, you must set the lfPitchAndFamily member to 0 (except in a couple of special cases we won't go into here). Having done all of that, you can call EnumFontFamiliesEx() to have Windows enumerate all the fonts:
EnumFontFamiliesEx(Canvas->Handle, &lf, (WNDENUMPROC)FontEnumProc, 0, 0);The first parameter takes a handle to a device context; here you use the canvas's Handle property. The second parameter takes a pointer to the LOGFONT structure you just created. The third parameter takes a pointer to the callback function, which will be called once for each font in the system. The cast to WNDENUMPROC is necessary to get the code to compile (Windows C programmers love that kind of thing). The fourth parameter can be used for any application-specific data you want to pass to the callback function (we won't use it in our case), and the final parameter is reserved and should always be 0. When you call EnumFontFamiliesEx(), Windows begins enumerating the fonts. The callback function is called once for each installed font. Let's take a look at the callback function, since that's where all the action takes place.
int CALLBACK EnumFontFamProc( ENUMLOGFONTEX *lpelfe, NEWTEXTMETRICEX *lpntme, long FontType, LPARAM lParam );As you can see, the function gives you four parameters. The lpelfe parameter is a pointer to a function that contains information about the font (more on that in just a bit). The lpntme parameter is a pointer to a structure that gives you the text metrics for the font; the FontType parameter tells you whether the font is a raster font, a device font, or a TrueType font; and the lParam parameter holds any user-defined data you want to pass to the function. The only parameter we're concerned with in this case is the lpelfe parameter--a pointer to an instance of the ENUMLOGFONTEX structure. (You could also make use of the FontType parameter if, for example, you wanted to limit the list to TrueType fonts.) This structure contains a data member called elfFullName, which contains the name of the font. You can add that text string directly to the combo box using the Add() method, as follows:
int CALLBACK FontEnumProc(
ENUMLOGFONTEX *lpelfe,
NEWTEXTMETRICEX *lpntme, long FontType,
LPARAM lParam)
{
Form1->FontCombo->Items->Add(
(char*)lpelfe->elfFullName);
return 1;
}
This code illustrates how to get the font name and add it to the combo box. In
a real-world application, you'll need to have some method of ensuring that the
combo box doesn't contain duplicate strings. You'll get duplicate strings
because Windows will execute the callback function for each font style in a
particular font family. For example the Arial font contains the typeface names
Arial Bold, Arial Italic, Arial Bold Italic, and so on. The elfFullName member
of the ENUMLOGFONTEX structure provides the font name itself, and the elfStyle
member contains the style. If desired, you could build the font list by
combining the elfFullName member with the elfStyle member.
You might think to set the combo box's Sorted property to true and the Duplicates property to false, thereby preventing duplicate strings in the combo box. For some odd reason, TComboBox::Items is of type TStrings rather than of type TStringList (as it is with list boxes), so there's no Duplicates property. Oh, well, I guess we'll have to do it the hard way. We won't show the code here, but see Listing B for an example of how to eliminate duplicate strings. After enumerating all of the fonts, EnumFontFamiliesEx() returns and the combo box contains a list of available fonts.
for (int i=8;i<40;i+=2) {
FontSizeCombo->Items->Add(i);
}
This code puts font sizes from 8 to 40 in the font size combo box, showing only
even font sizes. Some programs use a rather odd algorithm for displaying font
sizes in their font size selection combo boxes. You can follow those
conventions or you can just go the easy route and use our code.
Since you don't care which component was clicked, you'll set all the font attributes each time the OnClick handler is called. The main body of the OnClick handler contains this code:
Memo->Font->Name = FontCombo->Text; Memo->Font->Size = FontSizeCombo->Text.ToIntDef(10); TFontStyles styles; if (BoldBtn->Down) styles << fsBold; if (ItalicBtn->Down) styles << fsItalic; if (UnderlineBtn->Down) styles << fsUnderline; Memo->Font->Style = styles;You extract the font name from the font combo box and assign it to the Memo font's Name property. Since Windows gave you the font names, you know that any font the user selects is valid. Next, the code sets the font's Size property to the value contained in the font size combo box. As you can see, the AnsiString class's ToIntDef() method converts the text in the combo box to an integer. Remember, the user may type a font size directly rather than choosing a font size from the dropdown list. The ToIntDef() method ensures that if the user types invalid characters, VCL will supply a default value (10) rather than throwing an exception.
Finally, you read the states of the three font style buttons and build a TFontStyles set based on which buttons are down. After that, you assign the results of the set to the Style property of the Memo's font.
Listing A: FONTBARU.H
//---------------------------------------------
#ifndef FontBarUH
#define FontBarUH
/----------------------------------------------
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\ExtCtrls.hpp>
#include <vcl\Buttons.hpp>
#include <vcl\ComCtrls.hpp>
//---------------------------------------------
class TForm1 : public TForm
{
__published: // IDE-managed Components
TPanel *Panel1;
TComboBox *FontCombo;
TComboBox *FontSizeCombo;
TSpeedButton *BoldBtn;
TSpeedButton *ItalicBtn;
TSpeedButton *UnderlineBtn;
TRichEdit *RichEdit1;
TMemo *Memo;
void __fastcall FormCreate(TObject *Sender);
void __fastcall FontBarClick(TObject *Sender);
void __fastcall FontSizeComboKeyPress
(TObject *Sender, char &Key);
private: // User declarations
public: // User declarations
__fastcall TForm1(TComponent* Owner);
};
//---------------------------------------------
extern TForm1 *Form1;
Listing B: FONTBARU.CPP
//---------------------------------
------------#include <vcl\vcl.h>
#pragma hdrstop
#include "FontBarU.h"
//-----------------------------------
----------#pragma resource "*.dfm"
TForm1 *Form1;
int CALLBACK FontEnumProc(ENUMLOGFONTEX *lpelfe,
NEWTEXTMETRICEX *lpntme, long FontType,
LPARAM lParam)
{
// static String object to hold text of last font
static String S;
// If the string is different, then save it for
// the next time and add it to the combo box.
if (S != String((char*)lpelfe->elfFullName)) {
S = (char*)lpelfe->elfFullName;
Form1->FontCombo->Items->Add(S);
}
// Return a non-zero value to keep iterating.
return 1;
}
//--------------------------------------
-------__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//-------------------------------------
--------void __fastcall TForm1::FormCreate
(TObject *Sender)
{
// Set up the logfont structure with the
// values required to enumerate all fonts.
LOGFONT lf;
lf.lfCharSet = DEFAULT_CHARSET;
strcpy(lf.lfFaceName, "");
lf.lfPitchAndFamily = 0;
// Start enumerating, passing the address
// of the callback function.
EnumFontFamiliesEx(Canvas->Handle,
&lf, (WNDENUMPROC)FontEnumProc, 0, 0);
// Fill in the font size combo box.
for (int i=8;i<40;i+=2)
FontSizeCombo->Items->Add(i);
// Select the "10" item in the combo box
// since the initial point size is 10.
FontSizeCombo->ItemIndex =
FontSizeCombo->Items->IndexOf("10");
// Select the Courier New font so the font
// combo box shows the current font when
// initially displayed.
FontCombo->ItemIndex =
FontCombo->Items->IndexOf("Courier New");
// Set the initial font and font size, and
// load this program's source into the memo.
Memo->Font->Name = "Courier New";
Memo->Font->Size = 10;
Memo->Lines->LoadFromFile("FontBarU.cpp");
}
//------------------------------------
---------void __fastcall
TForm1::FontBarClick(TObject *Sender)
{
// Set the Font's Name property to the font
// selected in the combo box.
Memo->Font->Name = FontCombo->Text;
// Set the font Size to the value in the
// font size combo box.
Memo->Font->Size =
FontSizeCombo->Text.ToIntDef(10);
// Set the Style as needed based on which
// of the style buttons are down.
TFontStyles styles;
if (BoldBtn->Down) styles << fsBold;
if (ItalicBtn->Down) styles << fsItalic;
if (UnderlineBtn->Down) styles << fsUnderline;
Memo->Font->Style = styles;
}
//---------------------------------------------
void __fastcall TForm1::FontSizeComboKeyPress
(TObject *Sender, char &Key)
{
// If the enter key on the font size combo
// box was pressed then swallow the keystroke
// and call FontBarClick() to update the
// font in the memo component.
if (Key == VK_RETURN) {
Key = 0;
FontBarClick(0);
Memo->SetFocus();
}
}
Figure B: Our sample application lets you control the Memo control's font
characteristics.
You'll see that we've added a few niceties to make the toolbar behave properly. For example, the size value changes when the user presses [Enter] in the font size combo box. It's natural to press [Enter] after typing a new value, but this action will result in an annoying beep and nothing more unless you write code to account for that possibility. As you can see in Listing B, the code simply responds to the OnKeyPress event and converts the [Enter] keypress into a 0, eliminating the beep. After that, you call the FontBarClick() method to update the font.
Now that you have the basic idea, it will be relatively simple to add text-alignment buttons next to the font-style buttons. You could add buttons for left-justified, centered, or right-justified text. You'll have to change the Memo component to a RichEdit component, but for the most part it's a trivial exercise. One hint: Change the GroupIndex value for the alignment buttons to the same value. Doing so will ensure that only one of the buttons can be down at a time. Also, provide OnClick handlers for each of the buttons. For example, the handler for the Center button would contain just one line of code:
RichEdit->Paragraph->Alignment = taCenter;