One attribute of a device context that you should be aware of when using dithered brush colors or hatch brushes is the brush origin. When Windows fills an area with a hatched or dithered brush pattern, it tiles an 8-pixel by 8-pixel pattern horizontally and vertically within the affected area. By default, the origin for this pattern, better known as the brush origin, is the device point (0,0)—the screen pixel in the upper left corner of the window. This means that a pattern drawn in a rectangle that begins 100 pixels to the right of and below the origin will be aligned somewhat differently with respect to the rectangle's border than a pattern drawn in a rectangle positioned a few pixels to the left or right, as shown in Figure 2-8. In many applications, it doesn't matter; the user isn't likely to notice minute differences in brush alignment. However, in some situations it matters a great deal.
Figure 2-8. Brush alignment.
Suppose you're using a hatch brush to fill a rectangle and you're animating the motion of that rectangle by repeatedly erasing it and redrawing it 1 pixel to the right or the left. If you don't reset the brush origin to a point that stays in the same position relative to the rectangle before each redraw, the hatch pattern will "walk" as the rectangle moves.
The solution? Before selecting the brush into the device context and drawing the rectangle, call CGdiObject::UnrealizeObject on the brush object to permit the brush origin to be moved. Then call CDC::SetBrushOrg to align the brush origin with the rectangle's upper left corner, as shown here:
CPoint point (x1, y1); dc.LPtoDP (&point); point.x %= 8; point.y %= 8; brush.UnrealizeObject (); dc.SetBrushOrg (point); dc.SelectObject (&brush); dc.Rectangle (x1, y1, x2, y2);
In this example, point is a CPoint object that holds the logical coordinates of the rectangle's upper left corner. LPtoDP is called to convert logical coordinates into device coordinates (brush origins are always specified in device coordinates), and a modulo-8 operation is performed on the resulting values because coordinates passed to SetBrushOrg should fall within the range 0 through 7. Now the hatch pattern will be aligned consistently no matter where in the window the rectangle is drawn.
Drawing TextYou've already seen one way to output text to a window. The CDC::DrawText function writes a string of text to a display surface. You tell DrawText where to draw its output by specifying both a formatting rectangle and a series of option flags indicating how the text is to be positioned within the rectangle. In Chapter 1's Hello program, the statement
dc.DrawText (_T ("Hello, MFC"), -1, &rect, DT_SINGLELINE ¦ DT_CENTER ¦ DT_VCENTER);
drew "Hello, MFC" so that it was centered in the window. rect was a rectangle object initialized with the coordinates of the window's client area, and the DT_CENTER and DT_VCENTER flags told DrawText to center its output in the rectangle both horizontally and vertically.
DrawText is one of several text-related functions that are members of MFC's CDC class. Some of the others are listed in the table below. One of the most useful is TextOut, which outputs text like DrawText but accepts an x-y coordinate pair that specifies where the text will be output and also uses the current position if it is asked to. The statement
dc.TextOut (0, 0, CString (_T ("Hello, MFC")));
writes "Hello, MFC" to the upper left of the display surface represented by dc. A related function named TabbedTextOut works just like TextOut except that it expands tab characters into white space. (If a string passed to TextOut contains tabs, the characters show up as rectangles in most fonts.) Tab positions are specified in the call to TabbedTextOut. A related function named ExtTextOut gives you the added option of filling a rectangle surrounding the output text with an opaque background color. It also gives the programmer precise control over intercharacter spacing.
By default, the coordinates passed to TextOut, TabbedTextOut, and ExtTextOut specify the location of the upper left corner of the text's leftmost character cell. However, the relationship between the coordinates passed to TextOut and the characters in the output string, a property known as the text alignment, is an attribute of the device context. You can change it with CDC::SetTextAlign. For example, after a
dc.SetTextAlign (TA_RIGHT);
statement is executed, the x coordinate passed to TextOut specifies the rightmost position in the character cell—perfect for drawing right-aligned text.
You can also call SetTextAlign with a TA_UPDATECP flag to instruct TextOut to ignore the x and y arguments passed to it and use the device context's current position instead. When the text alignment includes TA_UPDATECP, TextOut updates the x component of the current position each time a string is output. One use for this feature is to achieve proper spacing between two or more character strings that are output on the same line.
CDC Text Functions
Function Description DrawText Draws text in a formatting rectangle TextOut Outputs a line of text at the current or specified position TabbedTextOut Outputs a line of text that includes tabs ExtTextOut Outputs a line of text and optionally fills a rectangle with a background color or varies the intercharacter spacing GetTextExtent Computes the width of a string in the current font GetTabbedTextExtent Computes the width of a string with tabs in the current font GetTextMetrics Returns font metrics (character height, average character width, and so on) for the current font SetTextAlign Sets alignment parameters for TextOut and other output functions SetTextJustification Specifies the added width that is needed to justify a string of text SetTextColor Sets the device context's text output color SetBkColor Sets the device context's background color, which determines the fill color used behind characters that are output to a display surface
Two functions—GetTextMetrics and GetTextExtent—let you retrieve information about the font that is currently selected into the device context. GetTextMetrics fills a TEXTMETRIC structure with information on the characters that make up the font. GetTextExtent returns the width of a given string, in logical units, rendered in that font. (Use GetTabbedTextExtent if the string contains tab characters.) One use for GetTextExtent is to gauge the width of a string prior to outputting it in order to compute how much space is needed between words to fully justify the text. If nWidth is the distance between left and right margins, the following code outputs the text "Now is the time" and justifies the output to both margins:
CString string = _T ("Now is the time"); CSize size = dc.GetTextExtent (string); dc.SetTextJustification (nWidth - size.cx, 3); dc.TextOut (0, y, string);
The second parameter passed to SetTextJustification specifies the number of break characters in the string. The default break character is the space character. After SetTextJustification is called, subsequent calls to TextOut and related text output functions distribute the space specified in the SetTextJustification's first parameter evenly between all the break characters.
GDI Fonts and the CFont ClassAll CDC text functions use the font that is currently selected into the device context. A font is a group of characters of a particular size (height) and typeface that share common attributes such as character weight—for example, normal or boldface. In classical typography, font sizes are measured in units called points. One point equals about 1/72 inch. Each character in a 12-point font is nominally 1/6 inch tall, but in Windows, the actual height can vary somewhat depending on the physical characteristics of the output device. The term typeface describes a font's basic style. Times New Roman is one example of a typeface; Courier New is another.
A font is a GDI object, just as a pen or a brush is. In MFC, fonts are represented by objects of the CFont class. Once a CFont object is constructed, you create the underlying GDI font by calling the CFont object's CreateFont, CreateFontIndirect, CreatePointFont, or CreatePointFontIndirect function. Use CreateFont or CreateFontIndirect if you want to specify the font size in pixels, and use CreatePointFont and CreatePointFontIndirect to specify the font size in points. Creating a 12-point Times New Roman screen font with CreatePointFont requires just two lines of code:
CFont font; font.CreatePointFont (120, _T ("Times New Roman"));
Doing the same with CreateFont requires you to query the device context for the logical number of pixels per inch in the vertical direction and to convert points to pixels:
CClientDC dc (this); int nHeight = -((dc.GetDeviceCaps (LOGPIXELSY) * 12) / 72); CFont font; font.CreateFont (nHeight, 0, 0, 0, FW_NORMAL, 0, 0, 0, DEFAULT_CHARSET, OUT_CHARACTER_PRECIS, CLIP_CHARACTER_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH ¦ FF_DONTCARE, _T ("Times New Roman"));
Incidentally, the numeric value passed to CreatePointFont is the desired point size times 10. This allows you to control the font size down to 1/10 point—plenty accurate enough for most applications, considering the relatively low resolution of most screens and other commonly used output devices.
The many parameters passed to CreateFont specify, among other things, the font weight and whether characters in the font are italicized. You can't create a bold, italic font with CreatePointFont, but you can with CreatePointFontIndirect. The following code creates a 12-point bold, italic Times New Roman font using CreatePointFontIndirect.
LOGFONT lf; ::ZeroMemory (&lf, sizeof (lf)); lf.lfHeight = 120; lf.lfWeight = FW_BOLD; lf.lfItalic = TRUE; ::lstrcpy (lf.lfFaceName, _T ("Times New Roman")); CFont font; font.CreatePointFontIndirect (&lf);
LOGFONT is a structure whose fields define all the characteristics of a font. ::ZeroMemory is an API function that zeroes a block of memory, and ::lstrcpy is an API function that copies a text string from one memory location to another. You can use the C run time's memset and strcpy functions instead (actually, you should use _tcscpy in lieu of strcpy so the call will work with ANSI or Unicode characters), but using Windows API functions frequently makes an executable smaller by reducing the amount of statically linked code.
After creating a font, you can select it into a device context and draw with it using DrawText, TextOut, and other CDC text functions. The following OnPaint handler draws "Hello, MFC" in the center of a window. But this time the text is drawn using a 72-point Arial typeface, complete with drop shadows. (See Figure 2-9.)
void CMainWindow::OnPaint () { CRect rect; GetClientRect (&rect); CFont font; font.CreatePointFont (720, _T ("Arial")); CPaintDC dc (this); dc.SelectObject (&font); dc.SetBkMode (TRANSPARENT); CString string = _T ("Hello, MFC"); rect.OffsetRect (16, 16); dc.SetTextColor (RGB (192, 192, 192)); dc.DrawText (string, &rect, DT_SINGLELINE ¦ DT_CENTER ¦ DT_VCENTER); rect.OffsetRect (-16, -16); dc.SetTextColor (RGB (0, 0, 0)); dc.DrawText (string, &rect, DT_SINGLELINE ¦ DT_CENTER ¦ DT_VCENTER); }
Figure 2-9. "Hello, MFC" rendered in 72-point Arial with drop shadows.
The shadow effect is achieved by drawing the text string twice—once a few pixels to the right of and below the center of the window, and once in the center. MFC's CRect::OffsetRect function makes it a snap to "move" rectangles by offsetting them a specified distance in the x and y directions.
What happens if you try to create, say, a Times New Roman font on a system that doesn't have Times New Roman installed? Rather than fail the call, the GDI will pick a similar typeface that is installed. An internal font-mapping algorithm is called to pick the best match, and the results aren't always what one might expect. But at least your application won't output text just fine on one system and mysteriously output nothing on another.
Raster Fonts vs. TrueType FontsMost GDI fonts fall into one of two categories: raster fonts and TrueType fonts. Raster fonts are stored as bitmaps and look best when they're displayed in their native sizes. One of the most useful raster fonts provided with Windows is MS Sans Serif, which is commonly used (in its 8-point size) on push buttons, radio buttons, and other dialog box controls. Windows can scale raster fonts by duplicating rows and columns of pixels, but the results are rarely pleasing to the eye due to stair-stepping effects.
The best fonts are TrueType fonts because they scale well to virtually any size. Like PostScript fonts, TrueType fonts store character outlines as mathematical formulas. They also include "hint" information that's used by the GDI's TrueType font rasterizer to enhance scalability. You can pretty much bank on the fact that any system your application runs on will have the following TrueType fonts installed, because all four are provided with Windows:
Times New Roman
Arial Courier New SymbolIn Chapter 7, you'll learn how to query the system for font information and how to enumerate the fonts that are installed. Such information can be useful if your application requires precise character output or if you want to present a list of installed fonts to the user.
Rotated TextOne question that's frequently asked about GDI text output is "How do I display rotated text?" There are two ways to do it, one of which works only in Microsoft Windows NT and Windows 2000. The other method is compatible with all 32-bit versions of Windows, so it's the one I'll describe here.
The secret is to create a font with CFont::CreateFontIndirect or CFont::CreatePointFontIndirect and to specify the desired rotation angle (in degrees) times 10 in the LOGFONT structure's lfEscapement and lfOrientation fields. Then you output the text in the normal manner—for example, using CDC::TextOut. Conventional text has an escapement and orientation of 0; that is, it has no slant and is drawn on a horizontal. Setting these values to 450 rotates the text counterclockwise 45 degrees. The following OnPaint handler increments lfEscapement and lfOrientation in units of 15 degrees and uses the resulting fonts to draw the radial text array shown in Figure 2-10:
void CMainWindow::OnPaint () { CRect rect; GetClientRect (&rect); CPaintDC dc (this); dc.SetViewportOrg (rect.Width () / 2, rect.Height () / 2); dc.SetBkMode (TRANSPARENT); for (int i=0; i<3600; i+=150) { LOGFONT lf; ::ZeroMemory (&lf, sizeof (lf)); lf.lfHeight = 160; lf.lfWeight = FW_BOLD; lf.lfEscapement = i; lf.lfOrientation = i; ::lstrcpy (lf.lfFaceName, _T ("Arial")); CFont font; font.CreatePointFontIndirect (&lf); CFont* pOldFont = dc.SelectObject (&font); dc.TextOut (0, 0, CString (_T (" Hello, MFC"))); dc.SelectObject (pOldFont); } }
This technique works great with TrueType fonts, but it doesn't work at all with raster fonts.
Figure 2-10. Rotated text.
Stock ObjectsWindows predefines a handful of pens, brushes, fonts, and other GDI objects that can be used without being explicitly created. Called stock objects, these GDI objects can be selected into a device context with the CDC::SelectStockObject function or assigned to an existing CPen, CBrush, or other object with CGdiObject::CreateStockObject. CGdiObject is the base class for CPen, CBrush, CFont, and other MFC classes that represent GDI objects.
The following table shows a partial list of the available stock objects. Stock pens go by the names WHITE_PEN, BLACK_PEN, and NULL_PEN. WHITE_PEN and BLACK_PEN draw solid lines that are 1 pixel wide. NULL_PEN draws nothing. The stock brushes include one white brush, one black brush, and three shades of gray. HOLLOW_BRUSH and NULL_BRUSH are two different ways of referring to the same thing—a brush that paints nothing. SYSTEM_FONT is the font that's selected into every device context by default.
Commonly Used Stock Objects
Object Description NULL_PEN Pen that draws nothing BLACK_PEN Black pen that draws solid lines 1 pixel wide WHITE_PEN White pen that draws solid lines 1 pixel wide NULL_BRUSH Brush that draws nothing HOLLOW_BRUSH Brush that draws nothing (same as NULL_BRUSH) BLACK_BRUSH Black brush DKGRAY_BRUSH Dark gray brush GRAY_BRUSH Medium gray brush LTGRAY_BRUSH Light gray brush WHITE_BRUSH White brush ANSI_FIXED_FONT Fixed-pitch ANSI font ANSI_VAR_FONT Variable-pitch ANSI font SYSTEM_FONT Variable-pitch system font SYSTEM_FIXED_FONT Fixed-pitch system font
Suppose you want to draw a light gray circle with no border. How do you do it? Here's one way:
CPen pen (PS_NULL, 0, (RGB (0, 0, 0))); dc.SelectObject (&pen); CBrush brush (RGB (192, 192, 192)); dc.SelectObject (&brush); dc.Ellipse (0, 0, 100, 100);
But since NULL pens and light gray brushes are stock objects, here's a better way:
dc.SelectStockObject (NULL_PEN); dc.SelectStockObject (LTGRAY_BRUSH); dc.Ellipse (0, 0, 100, 100);
The following code demonstrates a third way to draw the circle. This time the stock objects are assigned to a CPen and a CBrush rather than selected into the device context directly:
CPen pen; pen.CreateStockObject (NULL_PEN); dc.SelectObject (&pen); CBrush brush; brush.CreateStockObject (LTGRAY_BRUSH); dc.SelectObject (&brush); dc.Ellipse (0, 0, 100, 100);
Which of the three methods you use is up to you. The second method is the shortest, and it's the only one that's guaranteed not to throw an exception since it doesn't create any GDI objects.
Deleting GDI ObjectsPens, brushes, and other objects created from CGdiObject-derived classes are resources that consume space in memory, so it's important to delete them when you no longer need them. If you create a CPen, CBrush, CFont, or other CGdiObject on the stack, the associated GDI object is automatically deleted when CGdiObject goes out of scope. If you create a CGdiObject on the heap with new, be sure to delete it at some point so that its destructor will be called. The GDI object associated with a CGdiObject can be explicitly deleted by calling CGdiObject::DeleteObject. You never need to delete stock objects, even if they are "created" with CreateStockObject.
In 16-bit Windows, GDI objects frequently contributed to the problem of resource leakage, in which the Free System Resources figure reported by Program Manager gradually decreased as applications were started and terminated because some programs failed to delete the GDI objects they created. All 32-bit versions of Windows track the resources a program allocates and deletes them when the program ends. However, it's still important to delete GDI objects when they're no longer needed so that the GDI doesn't run out of memory while a program is running. Imagine an OnPaint handler that creates 10 pens and brushes every time it's called but neglects to delete them. Over time, OnPaint might create thousands of GDI objects that occupy space in system memory owned by the Windows GDI. Pretty soon, calls to create pens and brushes will fail, and the application's OnPaint handler will mysteriously stop working.
In Visual C++, there's an easy way to tell whether you're failing to delete pens, brushes, and other resources: Simply run a debug build of your application in debugging mode. When the application terminates, resources that weren't freed will be listed in the debugging window. MFC tracks memory allocations for CPen, CBrush, and other CObject-derived classes so that it can notify you when an object hasn't been deleted. If you have difficulty ascertaining from the debug messages which objects weren't deleted, add the statement
#define new DEBUG_NEW
to your application's source code files after the statement that includes Afxwin.h. (In AppWizard-generated applications, this statement is included automatically.) Debug messages for unfreed objects will then include line numbers and file names to help you pinpoint leaks.
Deselecting GDI ObjectsIt's important to delete the GDI objects you create, but it's equally important to never delete a GDI object while it's selected into a device context. Code that attempts to paint with a deleted object is buggy code. The only reason it doesn't crash is that the Windows GDI is sprinkled with error-checking code to prevent such crashes from occurring.
Abiding by this rule isn't as easy as it sounds. The following OnPaint handler allows a brush to be deleted while it's selected into a device context. Can you figure out why?
void CMainWindow::OnPaint () { CPaintDC dc (this); CBrush brush (RGB (255, 0, 0)); dc.SelectObject (&brush); dc.Ellipse (0, 0, 200, 100); }
Here's the problem. A CPaintDC object and a CBrush object are created on the stack. Since the CBrush is created second, its destructor gets called first. Consequently, the associated GDI brush is deleted before dc goes out of scope. You could fix this by creating the brush first and the DC second, but code whose robustness relies on stack variables being created in a particular order is bad code indeed. As far as maintainability goes, it's a nightmare.
The solution is to select the CBrush out of the device context before the CPaintDC object goes out of scope. There is no UnselectObject function, but you can select an object out of a device context by selecting in another object. Most Windows programmers make it a practice to save the pointer returned by the first call to SelectObject for each object type and then use that pointer to reselect the default object. An equally viable approach is to select stock objects into the device context to replace the objects that are currently selected in. The first of these two methods is illustrated by the following code:
CPen pen (PS_SOLID, 1, RGB (255, 0, 0)); CPen* pOldPen = dc.SelectObject (&pen); CBrush brush (RGB (0, 0, 255)); CBrush* pOldBrush = dc.SelectObject (&brush); dc.SelectObject (pOldPen); dc.SelectObject (pOldBrush);
The second method works like this:
CPen pen (PS_SOLID, 1, RGB (255, 0, 0)); dc.SelectObject (&pen); CBrush brush (RGB (0, 0, 255)); dc.SelectObject (&brush); dc.SelectStockObject (BLACK_PEN); dc.SelectStockObject (WHITE_BRUSH);
The big question is why this is necessary. The simple truth is that it's not. In modern versions of Windows, there's no harm in allowing a GDI object to be deleted a split second before a device context is released, especially if you're absolutely sure that no drawing will be done in the interim. Still, cleaning up a device context by deselecting the GDI objects you selected in is a common practice in Windows programming. It's also considered good form, so it's something I'll do throughout this book.
Incidentally, GDI objects are occasionally created on the heap, like this:
CPen* pPen = new CPen (PS_SOLID, 1, RGB (255, 0, 0)); CPen* pOldPen = dc.SelectObject (pPen);
At some point, the pen must be selected out of the device context and deleted. The code to do it might look like this:
dc.SelectObject (pOldPen); delete pPen;
Since the SelectObject function returns a pointer to the object selected out of the device context, it might be tempting to try to deselect the pen and delete it in one step:
delete dc.SelectObject (pOldPen);
But don't do this. It works fine with pens, but it might not work with brushes. Why? Because if you create two identical CBrushes, 32-bit Windows conserves memory by creating just one GDI brush and you'll wind up with two CBrush pointers that reference the same HBRUSH. (An HBRUSH is a handle that uniquely identifies a GDI brush, just as an HWND identifies a window and an HDC identifies a device context. A CBrush wraps an HBRUSH and stores the HBRUSH handle in its m_hObject data member.) Because CDC::SelectObject uses an internal table maintained by MFC to convert the HBRUSH handle returned by SelectObject to a CBrush pointer and because that table assumes a one-to-one mapping between HBRUSHes and CBrushes, the CBrush pointer you get back might not match the CBrush pointer returned by new. Be sure you pass delete the pointer returned by new. Then both the GDI object and the C++ object will be properly destroyed.
The Ruler ApplicationThe best way to get acquainted with the GDI and the MFC classes that encapsulate it is to write code. Let's start with a very simple application. Figure 2-12 contains the source code for Ruler, a program that draws a 12-inch ruler on the screen. Ruler's output is shown in Figure 2-11.
Figure 2-11. The Ruler window.
Figure 2-12. The Ruler application.
Ruler.h
class CMyApp : public CWinApp { public: virtual BOOL InitInstance (); }; class CMainWindow : public CFrameWnd { public: CMainWindow (); protected: afx_msg void OnPaint (); DECLARE_MESSAGE_MAP () };
Ruler.cpp
#include <afxwin.h> #include "Ruler.h" CMyApp myApp; ///////////////////////////////////////////////////////////////////////// // CMyApp member functions BOOL CMyApp::InitInstance () { m_pMainWnd = new CMainWindow; m_pMainWnd->ShowWindow (m_nCmdShow); m_pMainWnd->UpdateWindow (); return TRUE; } ///////////////////////////////////////////////////////////////////////// // CMainWindow message map and member functions BEGIN_MESSAGE_MAP (CMainWindow, CFrameWnd) ON_WM_PAINT () END_MESSAGE_MAP () CMainWindow::CMainWindow () { Create (NULL, _T ("Ruler")); } void CMainWindow::OnPaint () { CPaintDC dc (this); // // Initialize the device context. // dc.SetMapMode (MM_LOENGLISH); dc.SetTextAlign (TA_CENTER ¦ TA_BOTTOM); dc.SetBkMode (TRANSPARENT); // // Draw the body of the ruler. // CBrush brush (RGB (255, 255, 0)); CBrush* pOldBrush = dc.SelectObject (&brush); dc.Rectangle (100, -100, 1300, -200); dc.SelectObject (pOldBrush); // // Draw the tick marks and labels. // for (int i=125; i<1300; i+=25) { dc.MoveTo (i, -192); dc.LineTo (i, -200); } for (i=150; i<1300; i+=50) { dc.MoveTo (i, -184); dc.LineTo (i, -200); } for (i=200; i<1300; i+=100) { dc.MoveTo (i, -175); dc.LineTo (i, -200); CString string; string.Format (_T ("%d"), (i / 100) - 1); dc.TextOut (i, -175, string); } }
The structure of Ruler is similar to that of the Hello application presented in Chapter 1. The CMyApp class represents the application itself. CMyApp::InitInstance creates a frame window by constructing a CMainWindow object, and CMainWindow's constructor calls Create to create the window you see on the screen. CMainWindow::OnPaint handles all the drawing. The body of the ruler is drawn with CDC::Rectangle, and the hash marks are drawn with CDC::LineTo and CDC::MoveTo. Before the rectangle is drawn, a yellow brush is selected into the device context so that the body of the ruler will be painted yellow. Numeric labels are drawn with CDC::TextOut and positioned over the tick marks by calling SetTextAlign with TA_CENTER and TA_BOTTOM flags and passing TextOut the coordinates of the top of each tick mark. Before TextOut is called for the first time, the device context's background mode is set to TRANSPARENT. Otherwise, the numbers on the face of the ruler would be drawn with white backgrounds.
Rather than hardcode the strings passed to TextOut, Ruler uses CString::Format to generate text on the fly. CString is the MFC class that represents text strings. CString::Format works like C's printf function, converting numeric values to text and substituting them for placeholders in a formatting string. Windows programmers who work in C frequently use the ::wsprintf API function for text formatting. Format does the same thing for CString objects without requiring an external function call. And unlike ::wsprintf, Format supports the full range of printf formatting codes, including codes for floating-point and string variable types.
Ruler uses the MM_LOENGLISH mapping mode to scale its output so that 1 inch on the ruler corresponds to 1 logical inch on the screen. Hold a real ruler up to the screen and on most PCs you'll find that 1 logical inch equals a little more than 1 physical inch. If the ruler is output to a printer instead, logical inches and physical inches will match exactly.
本文地址:http://com.8s8s.com/it/it3574.htm