by Peter G. Aitken (c)1997
Originally published in Visual Developer magazine
Screen Capture the Visual Basic Way
One of the most useful Windows utilities is a screen capture program, as attested to by the abundance of screen capture utilities. People who write about computers use screen capture regularly, whether to illustrate a book or magazine article or to create figures as part of an in-house training manual. I'm sure there are plenty of other uses for screen capture. Thanks to the Windows API, capturing the screen image is not a difficult task, and in this column I'll show you how to do it in Visual Basic. We'll also take a look at precise form sizing, Visual Basic speed comparisons, and newsgroups that are of interest to the Visual Basic programmer.
Capturing the Screen in Visual Basic
Microsoft recognized the importance of being able to capture the screen by building a basic capture facility into Windows itself simply press PrintScreen or Alt+PrintScreen to copy an image of the entire screen or of the active window, respectively, to the clipboard. There are also plenty of specialized screen capture programs out there, and some of them are quite sophisticated. Even so, you may want to do it yourself, whether to include capture capability as part of a larger Visual Basic application you are writing, or simply to create a capture utility that works exactly the way you want it to.
The utility that I develop here is fairly simple, and is shown in Figure 1. When the Capture button is clicked, the program minimizes itself, captures the entire screen area, then reappears with the captured image displayed. Clicking Save lets you store the image on disk as a .BMP file. Because the program uses the Windows clipboard for temporary storage of the image, you can capture an image then paste it into any graphics program, such as PhotoShop, for additional manipulation or for saving in a different format.
When first starting to plan this program I considered the possibility of using Windows to get the screen image on the clipboard, then using Visual Basic to retrieve the image. Theoretically this should work fine, using the SendKeys() procedure to "press" the PrintScreen or Alt+PrintScreen key. I did not try this approach so cannot be sure that it would actually work. Rather, I decided to do everything within Visual Basic. This provides more flexibility as well as providing the opportunity to learn something about the inner workings of Windows. As you may already have guessed, we'll need to call on some Windows API functions to get the job done.
At the heart of it all is something that Windows calls a device context. You can think of a device context as an in-memory representation of a physical device, typically the screen or a printer. Your program interacts in a device-independent manner with the device context, and Windows (by means of drivers for the specific device) links your program to the actual device. If we create a device context for the screen, we can use it to write text or graphics to the screen. More important for the current project is that we can also use a screen device context to access the image that is presently displayed on the screen.
How is a device context related to a display context? A display context is a special type of device context that is connected to a specific window rather than to the entire screen. The Windows operating system treats each window as a separate display surface, sort of a virtual screen. With a display context a program is limited to writing text or graphics to the related window, and cannot access other parts of the screen. A screen device context, in contrast, permits access to the entire screen and for that reason can be a dangerous tool. Since we will be only reading data from the screen, however, there will be no problems.
Once we have created a screen device context, what's next? We need to convert the screen image data into a bitmap. You can't just throw any kind of data on the clipboard; the data has to be in one of several recognized formats, including the Windows bitmap format. This is not as simple as it may seem, requiring two steps.
First we need to create a compatible device context. A compatible device context has the same characteristics as a "real" device context, meaning that it is compatible with the associated device. However, it is not connected with any physical device but exists only in memory. Thus, if you create a device context that is compatible with the screen device, the compatible device context will have the same characteristics as the actual screen, such as pixel dimensions and color depth. You can use graphics statements to "draw" to a compatible device context, but you wont see the result until you transfer the final image from the compatible device context to the "real" device context. This technique is commonly used by Windows programs because transferring an image from one device context to another is a lot faster than the process of constructing the original image. For the screen capture utility we will do things backwards, transferring the existing screen image from the screen device context to a compatible device context.
Before we transfer the screen image to the compatible device context, however, we must put an empty bitmap in it. What exactly does this mean? In one sense every image in Windows is a bitmap because the individual picture elements, or pixels, are represented by data (bits) in memory. In this case, I'm referring to a special kind of Windows bitmap format that contains not only the pixel data but also a couple of header structures that contain additional information about the bitmap, such as its dimensions and color depth. A device context can do lots of things with a "plain" bitmap that is not in this special format, but for certain actions, including transferring the image to the clipboard, it is necessary that the image data be in this format. Having the image in Windows bitmap format will also simplify other program tasks, such as displaying it in an Image control and saving it to disk.
At this point most of the work is done. Once we have a copy of the screen image in the compatible device context, in Windows bitmap format, it is an easy matter to use API calls to copy the image to the clipboard and then from the clipboard into an Image control for display. From there, we can save the image to disk with Visual Basic's own SavePicture statement.
The project consists of one form module and one Basic module; code for these modules is given in Listings 1 and 2. The form contains an Image control with all properties at their default values, a Common Dialog control named CD1, and a control array of three command buttons with the captions &Capture, &Save, and E&xit. There are plenty of places for enhancements in this program, such as capturing part of the screen or staying hidden and capturing to an automatically generated filename with a hotkey. Once you know the secrets of screen capture, the bells and whistles are easy.
Curiously, I cannot get this program to work under Windows NT 4.0, but only with Windows 95. The problem is that the CreateDC() API function returns Null and not a valid device context. The reference material I have consulted says that CreateDC() should work the same under NT and under 95, but it does not, at least for me. I will continue to investigate and will report my findings in a future column.
|Note: Reader Phil Bunce worked out the code for capture in
NT, and it should work in 2000/XP as well. Instead of using
SrcDC = CreateDC("DISPLAY", 0, 0, 0)
change it to:
SrcDC = CreateDC("DISPLAY", vbNullString, vbNullString, 0)
Listing 1. Code for the Screen Capture utility's form module.
' Screen Capture. Captures the entire screen then
' saves the image to a bitmap file.
' Written by Peter G. Aitken
Private Sub ScreenCapture(Left As Long, Top As Long, _
Right As Long, Bottom As Long)
' Captures the specified area of the
' screen to the clipboard.
Dim capWidth As Long, capHeight As Long
Dim SrcDC As Long, DestDC As Long, hBMP As Long
' Calculate capture area size.
capWidth = Right - Left
capHeight = Bottom - Top
' Set up source and destination device contexts.
SrcDC = CreateDC("DISPLAY", 0, 0, 0)
DestDC = CreateCompatibleDC(SrcDC)
' Create a bitmap and select it into the destination DC.
hBMP = CreateCompatibleBitmap(SrcDC, capWidth, capHeight)
SelectObject DestDC, hBMP
' Transfer the bitmap from the screen DC to the
' destination DC.
BitBlt DestDC, 0, 0, capWidth, capHeight, SrcDC, Left, _
' Open the clipboard, empty it, then put
' the bitmap on it.
SetClipboardData 2, hBMP
' Clean up.
ReleaseDC 0, SrcDC
' Enable the Save button.
Command1(1).Enabled = True
Private Sub Command1_Click(Index As Integer)
Select Case Index
Case 0 ' Capture
Me.WindowState = vbMinimized
ScreenCapture 0, 0, _
Screen.Width / Screen.TwipsPerPixelX, _
Screen.Height / Screen.TwipsPerPixelY
Image1.Picture = Clipboard.GetData()
Me.WindowState = vbNormal
Case 1 ' Save
CD1.DefaultExt = "bmp"
CD1.Filter = "Bitmap file|*.bmp"
CD1.Flags = cdlOFNOverwritePrompt
If Trim(CD1.FileName) <> "" Then
SavePicture Image1.Picture, CD1.FileName
' Disable the Save button.
Command1(1).Enabled = False
Case 2 ' Exit
Private Sub Form_Load()
' Disable the Save button as there's
' nothing to save yet.
Command1(1).Enabled = False
Private Sub Form_Resize()
Dim i As Integer
' Position the three command buttons across
' the top of the form.
For i = 0 To 2
Command1(i).Move i * Me.ScaleWidth / 3, 0, _
Me.ScaleWidth / 3, CB_HEIGHT
' Size the Image control to fill the
' remainder of the form
If Me.WindowState <> vbMinimized Then
Image1.Left = 1
Image1.Top = CB_HEIGHT + 1
Image1.Width = Me.ScaleWidth
Image1.Height = Me.ScaleHeight - CB_HEIGHT
Listing 2. Code in the Screen Capture utility's Basic module.
' Height of the 3 command buttons
Global Const CB_HEIGHT = 400
' API function declarations.
Declare Function CloseClipBoard Lib "user32" Alias _
"CloseClipboard" () As Long
Declare Function BitBlt Lib "gdi32" (ByVal hDestDC As Long, _
ByVal x As Long, ByVal y As Long, ByVal nWidth As Long, _
ByVal nHeight As Long, ByVal hSrcDC As Long, ByVal xSrc _
As Long, ByVal ySrc As Long, ByVal dwRop As Long) As Long
Declare Function CreateDC Lib "gdi32" Alias "CreateDCA" _
(ByVal lpDriverName As String, ByVal lpDeviceName As _
String, ByVal lpOutput As String, lpInitData As String) _
Declare Function CreateCompatibleDC Lib "gdi32" _
(ByVal hdc As Long) As Long
Declare Function CreateCompatibleBitmap Lib "gdi32" _
(ByVal hdc As Long, ByVal nWidth As Long, _
ByVal nHeight As Long) As Long
Declare Function DeleteDC Lib "gdi32" (ByVal hdc As Long) _
Declare Function DeleteObject Lib "gdi32" _
(ByVal hObject As Long) As Long
Declare Function ReleaseDC Lib "user32" _
(ByVal hwnd As Long, ByVal hdc As Long) As Long
Declare Function SelectObject Lib "gdi32" _
(ByVal hdc As Long, ByVal hObject As Long) As Long
Declare Function SetClipboardData Lib "user32" _
(ByVal wFormat As Long, ByVal hMem As Long) As Long
Declare Function OpenClipboard Lib "user32" _
(ByVal hwnd As Long) As Long
Declare Function EmptyClipboard Lib "user32" () _
Sizing a Form to a Specified Size
While working on a recent graphics programming project, I wanted to display a form at an exact pixel dimensions. Why? The form contained a Picture Box control, in which I was going to display a bitmap. It was easy enough to automatically size the Picture Box to fill the form's client area by putting this code in the form's Resize event procedure:
Private Sub Form_Resize()
On Error Resume Next
Pic1.Move 0, 0, Me.ScaleWidth, _
It was not as easy for me to figure out how to make the form the exact same size as the bitmap (whose pixel dimensions I knew ahead of time). The problem is that the inner display area of the form the part inside the borders and title bar is what needed to be set to a specified size. However, the only way to size a form is by setting its Height and Width properties (either directly or by using the Move method) and these properties specify the form's outer dimensions in twips, not its inner dimensions in pixels.
Rather that wracking my brain I posted my question to one of the several Visual Basic newsgroups (more on these below). An elegant solution was quickly forthcoming from someone on the group. While I do not remember the individual's name, he or she has my thanks. Here's how it works:
Once the form is loaded it has a "size" even though it has not been displayed yet. By setting its ScaleMode to Pixels and reading the ScaleWidth and ScaleHeight properties we can determine the current pixel dimensions of the form's inside area. Comparing these values to the desired pixel dimensions gives us the change in size, in pixels, that is required.
Next, we read the Screen object's TwipsPerPixelX and TwipsPerPixelY properties to find out how many twips are in each horizontal and vertical pixel (twips, you'll recall, are Visuak Basic's default unit of screen measrement). Multiplying these values by the desired dimension change in pixels tells us how many twips the form's size must be changed by. Note that because the size of the form's borders and title bar remain constant, changing the form's overall size (the Height and Width properties) by a specified amount changes the size of the internal area by the same amount.
Once these calculations are complete it is a simple matter to use the Move method to resize the form. Here's the code for a general purpose procedure that can set any form to have a specified internal size expressed in pixels:
Public Sub SizeFormByPixels(frm As Form, _
nWidth As Integer, nHeight As Integer)
Dim OffsetX As Integer
Dim OffsetY As Integer
Dim NewSizeX As Integer
Dim NewSizeY As Integer
Dim OldScaleMode As Integer
OldScaleMode = .ScaleMode
.ScaleMode = vbPixels
OffsetX = .ScaleWidth - nWidth
OffsetY = .ScaleHeight - nHeight
NewSizeX = .Width - (Screen.TwipsPerPixelX _
NewSizeY = .Height - (Screen.TwipsPerPixelY _
.Move .Left, .Top, NewSizeX, NewSizeY
.ScaleMode = OldScaleMode
Then, in the appropriate location in the form's code call the procedure as follows:
Call SizeFormByPixels(Me, PixWidth, PixHeight)
As written, the procedure does not change the form's position. It would be a simple matter to modify the code to center the form on screen, within its parent MDI form, or whatever you like.
Visual Basic Newsgroups
It has been said that two heads are better than one. Assuming that each head is on its own body, I agree! It follows that several hundred heads may be better yet, and when you're facing a thorny programming problem it is certainly true. But where can you get these "heads?" Most of us are lucky if we have one or two other Visual Basic programmers to consult, and if you're hidden away in a cabin in Montana you may be limited to asking the bears and elk (who, I understand, prefer Delphi anyway). Fortunately, with a phone line, modem, and newsreader software you can tap into a huge source of programming knowledge.
Where is this treasure trove? I am talking about Usenet, more popularly known as newsgroups. I expect that most readers of this column know about newsgroups already, but you may not know just how much Visual Basic information is out there. I count 29 different newsgroups that are devoted to various aspects of Visual Basic programming, and there may be more because there is no guarantee that my news server carries all of them. Let's take a look.
First there are those that fall in Usenet's comp hierarchy. Newgroups are arranged by topic, and comp is the top-level hierarchy devoted to computer-related topics. These newsgroups are moderated, which means that one or more people have the task of screening incoming posts. This should not be thought of as censorship, but rather serves to keep the messages on-topic, to weed out spam, to keep flames within reasonable limits, and so forth. There are 5 newsgroups under comp that are of direct interest to Visual Basic programmers:
Why, you may ask, are there both comp.lang.basic.visual and comp.lang.visual.basic newsgroups? To be honest I do not know, as the articles posted to the two groups do not seem to differ in topic.
Second are the newsgroups hosted by Microsoft on their public news server at msnews.microsoft.com. There are 24 distinct Visual Basic groups, all listed as microsoft.public.vb.xxxx. The xxxx parts are listed here:
You can see that just about any Visual Basic programming topic you can image receives coverage in the Microsoft public newsgroups. An added advantage of these groups is that some of Microsoft's own Visual Basic people seem to hang out there, and what better source of information that the horse's own mouth? Be aware that these newsgroups are not an official support mechanism, but they are often the quickest way to get an answer to your programming questions.
I find myself browsing a few of the Visual Basic newsgroups on a regular basis even when I do not have a specific question that needs answering. I often pick up useful tidbits of information, learn about better ways of doing things, or just have fun chatting with other programmers. I also feel that it is important to contribute to the groups. If everyone only asked questions and never answered any, where would we be? You may feel that you're not enough of an "expert" to be answering questions, but there's almost always someone who is more of a beginner than you are who can use your help.
If someone were to poll all Visual Basic programmers and ask what the most important new feature is in Visual Basic 5, I bet that the most common answer would be native code compilation. Most important, of course, is that it deprives Delphi proponents of their most important argument in their attempts to shown that Delphi is superior to Visual Basic. But seriously, folks, native code does offer the real advantage of faster program execution. Furthermore, since Visual Basic 5 uses the same compiler technology as Visual C++, which has a deserved reputation for being fast, we might expect some real improvements over Visual Basic 4. Just how much of an improvement, and how does Visual Basic 5 compare with other development tools?
The process of performing benchmarks is fraught with peril. The goal is to obtain some numerical indication of the perceived speed of applications created with a particular development tool. It is this somewhat nebulous "perceived speed" that users care about, and at best a benchmark can provide some numerical indication that programs written with compiler X will be perceived to run a lot faster, a little faster, or about the same as programs written with compiler Y. Then there are always arguments about what code should go into the benchmark programs. Fast math operations may not influence the execution of a word processing program, for example. There are no perfect benchmarks, and someone will always complain, particularly if the results show that their favorite development tool is not as fast as they "know" it is!
All this being said, let's take a look at what I did. I compared Visual Basic 4.0 (32 bit), Visual Basic 5 native code, Visual C++ 5.0, Delphi 2.0, and Visual J++ 1.1. All tests were run on the same system, a 200MHz Pentium/MMX with 64MB of RAM and no network connections. I rebooted before each run to ensure that the system was in the same state each time. Timing was done with each language's own time-related statements. To the extent permitted by each development environment, compiler options were set to favor fast code over small code. I devised three test programs that each executed a block of code 1 million times in a loop:
- The math suite performed a variety of integer and floating point operations: addition, subtraction, division, exponentiation, multiplication, and modulus.
- The string suite performed search, concatenation, and extraction operations.
- The graphics suite performed a sequence of drawing and pixel manipulation operations, using the language's native statements rather than API calls.
The results are shown in Figure 2. In this graph, larger numbers mean faster performance. I have normalized the values so that the result for Visual Basic 4 is expressed as 1 in each category.
What conclusions can we draw from this chart keeping in mind, of course, the inherent limitations of any benchmark tests as discussed above. We see that version 5 of Visual Basic is faster than version 4 in all categories, but not strikingly so except for graphics. I'm not sure whether this means that the old P-code was better than we thought, or that the native code compiler needs some more tweaking to provide optimized output. We also see that Visual C++ is quite a bit faster in all categories. Delphi is the real speed demon for math and string operations, but falls behind slightly for graphics. The results for Visual J++ are difficult to understand. Compared with Visual Basic 5, VJ++ is slightly faster for math, significantly slower for graphics, and abysmally slow for string operations. In fact, the value for Visual J++'s string benchmark is so low that the bar may not be visible on the chart!
What can we make of these results? To be honest I am a bit disappointed with the string and math results for Visual Basic 5. I had hoped that the move to native code compilation would provide a greater speed boost over the P-code of version 4, and would bring Visual Basic's performance more in line with Visual C++ and Delphi. On the bright side, the graphics performance has improved a great deal, and I think that fast display of graphics is a more important factor in a program's perceived speed than are rapid math and string manipulation. After all, how much time does a program actually spend crunching numbers or strings? Except for very specialized applications, not much. You wouldnt choose a development tool based solely on benchmark results any more than you would choose a car based on top speed comparisons.