ScrollWindow.bas
By
Frederick J. Harris
9/10/2007
This is the 9th post of my tutorial on beginning Api coding in PowerBASIC. At http://www.jose.it-berater.org/smfforum/index.php?topic=1243.0 is the first. There can also be found descriptions and links to the others. A comment link is at http://www.jose.it-berater.org/smfforum/index.php?topic=1255.0. This next program in my Scrolling lesson, ScrollWindow.bas, is considerably more complicated than Simplest.bas (Tutorial #8), so I want you to be forewarned! Simplest.bas uses what I might term a 'brute force' scrolling method, which, while fairly easy to understand and get working, isn't really efficient or elegant. It uses a for loop with incrementing or decrementing beginning and end points to write successive lines of text to the display. The inefficiency is that to see just one more line of text in either direction, the display has to laboriously run through a loop and refresh the whole screen. Due to the ineficient nature of refreshing a display using such high level language constructs as for loops with TextOut(), it is not unusual for screen flickering and shimmer to result from this technique.
A more optimized technique is to use a special Windows Api call named logically ScrollWindow(). With this technique the Windows Graphical Device Interface ( GDI ) internally shifts everything on the screen either up or down a programmer specified amount. Since the data is moved internally (probably with highly optimized assembler string primitives) in a very rapid manner, it tends to be much smoother than the brute force method we used in Simplest.bas. The main catch is that the technique leaves a blank line either at the top or bottom of the window (or the edges) which the programmer must write him/herself. If any of my readers in the distant and all but forgotten past may have indulged in 16 bit DOS assembler, what ScrollWindow() in essense does is similar to the old BIOS int 10H function #'s 6 and 7 Scroll Interrupts. To explain this lets take another look at a PAINTSTRUCT Type like we used in every WM_PAINT message handler so far in this series...
TYPE PAINTSTRUCT
hDC AS DWORD
fErase AS LONG
rcPaint AS RECT
fRestore AS LONG
fIncUpdate AS LONG
rgbReserved(0 TO 31) AS BYTE
END TYPE
There is a member variable of this Type which I havn't covered yet in this tutorial but which figures very prominently in the use of the ScrollWindow() call and that variable is rcPaint As RECT. When a call is made to ScrollWindow() or ScrollWindowEx(), Windows invalidates only that part of the window which has been uncovered by the scrolling operation, and if for example, the screen is only shifted up or down a distance of one line of text, that invalid region (a rectangle structure) may be only a small part of the window. In other words, instead of the programmer having to write code to repaint the whole window, he/she only needs to read the coordinates of that smaller invalid region, and TextOut() or DrawText() only those one or two missing lines. An example is certainly in order here to demonstrate the concept. Below is ScrollWindow1.bas. Please copy it to a code editor (This program can also be found attached), compile and run the program. When you do, click the scroll down button 5 or 6 times to see what happens. After your initial dismay at the results, please close the program, and open up the Output.txt file that would have been created in the directory where you ran the program. We'll examine it directly.
#Compile Exe 'Uses ScrollWindow() in conjunction with SetScrollInfo().
#Dim All 'Don't even think about not using this!
#Include "Win32api.inc" 'You ought to spend some considerable time examining this file with Notepad.
Global strLine() As String 'Dynamic strings are nice. Unfortunately, you need to be a C programmer to
%LAST_LINE=25 'really appreciate them!
Global fp As Long
Type WndEventArgs 'Package frequently needed parameters
hIns As Dword
hWnd As Dword
wParam As Dword
lParam As Dword
cyChar As Dword 'Verticle Size of a Character
iBegin As Long 'Offset into array of first line (array holds lines of text to scroll)
iScrollRange As Long 'iBegin to iEnd, as long as these values are in iScrollRange
iLinesVisible As Long 'One based count of lines visible on screen, adjusted as user resizes screen.
lpsi As SCROLLINFO 'Type / structure Microsoft now wishes us to use to set scroll position and
End Type 'scroll range
Function fnWndProc_OnCreate(wea As WndEventArgs) As Long
Local ps As PAINTSTRUCT
Local tm As TEXTMETRIC
Register i As Dword
Local hDC As Dword
Local rc As RECT
fp=Freefile
Open "Output.txt" For Output As #fp
Print #fp, "Entering fnWndProc_OnCreate()"
ReDim strLine(%LAST_LINE) 'Allocate memory buffer for lines of text to scroll
For i = 0 To UBound(strLine,1) 'Fill buffers with lines of text
strLine(i)=Str$(i)+" "+"PowerBASIC Scrolling Demo!"
Next i
hDC=GetDC(wea.hWnd) 'In Windows, you need something called a 'Device Context' to get
Call GetTextMetrics(hDC,tm) 'info on how things such as text will be displayed. To find out
wea.cyChar=tm.tmHeight 'how many lines of text will be visible on any screen size chosen
Call ReleaseDC(wea.hWnd,hDC) 'by a user, we need the height of a character, 'wea.cyChar'.
wea.lpsi.cbSize=SizeOf(wea.lpsi) 'Api Help wants us to specify size of SCROLLINFO structure.
Call GetClientRect(wea.hWnd,rc)
wea.iBegin=0
wea.iLinesVisible=Fix(rc.nBottom/wea.cyChar) '200 / 16 = 12.5 but 200 \ 16 = 12
Print #fp,"wea.iLinesVisible="wea.iLinesVisible
wea.iScrollRange=%LAST_LINE-wea.iLinesVisible+1 '25 - 12 + 1 = 14
Print #fp, "wea.iScrollRange="wea.iScrollRange
wea.lpsi.fMask=%SIF_ALL
wea.lpsi.nMin=0 : wea.lpsi.nMax=wea.iScrollRange
wea.lpsi.nPos=wea.iBegin
Call SetScrollInfo(wea.hWnd,%SB_VERT,wea.lpsi,%TRUE)
Print #fp, "wea.cyChar = "wea.cyChar
Print #fp, "Leaving fnWndProc_OnCreate()"
Print #fp, : Print #fp,
fnWndProc_OnCreate=0
End Function
Function fnWndProc_OnVScroll(wea As WndEventArgs) As Long
Select Case LoWrd(wea.wParam)
Case %SB_LINEUP
If wea.iBegin Then
Decr wea.iBegin
Call ScrollWindow(wea.hWnd,0,wea.cyChar,ByVal 0,ByVal 0)
wea.lpsi.fMask=%SIF_POS
wea.lpsi.nPos=wea.iBegin
Call SetScrollInfo(wea.hWnd,%SB_VERT,wea.lpsi,%TRUE)
End If
Case %SB_LINEDOWN
If wea.iBegin+wea.iLinesVisible<=%LAST_LINE Then
Incr wea.iBegin
Call ScrollWindow(wea.hWnd,0,-wea.cyChar,ByVal 0,ByVal 0)
wea.lpsi.fMask=%SIF_POS
wea.lpsi.nPos=wea.iBegin
Call SetScrollInfo(wea.hWnd,%SB_VERT,wea.lpsi,%TRUE)
End If
End Select
fnWndProc_OnVScroll=0
End Function
Function fnWndProc_OnPaint(wea As WndEventArgs) As Long
Local iStart As Long, iFinish As Long,iLine As Long
Static iPainted As Long '<<< 'This is the variable
Local ps As PAINTSTRUCT 'I used to sabotage
Register i As Dword 'this program. After
Local hDC As Dword 'three paint messages
'the program won't
Print #fp, "Entering fnWndProc_OnPaint()" 'paint the area
hDC=BeginPaint(wea.hWnd,ps) 'uncovered by the
Print #fp, "wea.iBegin = "wea.iBegin 'ScrollWindow() call
Print #fp, "wea.iScrollRange = "wea.iScrollRange 'anymore, and the text
Print #fp, "wea.iLinesVisible = "wea.iLinesVisible 'will gradually scroll
If iPainted<3 Then 'off the screen! Not
iStart=ps.rcPaint.nTop\wea.cyChar 'good!
iFinish=Ceil(ps.rcPaint.nBottom/wea.cyChar)-1
Print #fp, "ps.rcPaint.nTop = "ps.rcPaint.nTop 'We only get
Print #fp, "ps.rcPaint.nBottom = "ps.rcPaint.nBottom 'inside this if
Print #fp, "iStart = "iStart 'statement for
Print #fp, "iFinish = "iFinish 'the 1st three
Print #fp, 'WM_PAINTs
Print #fp, "i i*wea.cyChar iLine strLine(iLine)"
Print #fp, "======================================================================"
For i=iStart To iFinish
iLine=wea.iBegin+i
If iLine<=%LAST_LINE Then
Print #fp, i,i*wea.cyChar,iLine,strLine(iLine)
Call TextOut(hDC,0,i*wea.cyChar,ByVal StrPtr(strLine(iLine)),Len(strLine(iLine)))
End If
Next i
Incr iPainted
End If
Call EndPaint(wea.hWnd,ps)
Print #fp,
Print #fp, "Leaving fnWndProc_OnPaint()"
Print #fp, : Print #fp, : Print #fp,
fnWndProc_OnPaint=0
End Function
Function fnWndProc_OnClose(wea As WndEventArgs) As Long
Close #fp
Erase strLine
Call PostQuitMessage(0)
fnWndProc_OnClose=0
End Function
Function fnWndProc(ByVal hWnd As Long,ByVal wMsg As Long,ByVal wParam As Long,ByVal lParam As Long) As Long
Static wea As WndEventArgs
Select Case wMsg
Case %WM_CREATE
wea.hWnd=hWnd: wea.wParam=wParam: wea.lParam=lParam
fnWndProc=fnWndProc_OnCreate(wea)
Exit Function
Case %WM_VSCROLL
wea.hWnd=hWnd: wea.wParam=wParam: wea.lParam=lParam
fnWndProc=fnWndProc_OnVScroll(wea)
Exit Function
Case %WM_PAINT
wea.hWnd=hWnd: wea.wParam=wParam: wea.lParam=lParam
fnWndProc=fnWndProc_OnPaint(wea)
Exit Function
Case %WM_CLOSE
wea.hWnd=hWnd: wea.wParam=wParam: wea.lParam=lParam
fnWndProc=fnWndProc_OnClose(wea)
Exit Function
End Select
fnWndProc=DefWindowProc(hWnd,wMsg,wParam,lParam)
End Function
Function WinMain(ByVal hIns As Long,ByVal hPrevIns As Long,ByVal lpCmdLn As Asciiz Ptr,ByVal iShow As Long) As Long
Local hMainWnd As Dword,dwStyle As Dword
Local winclass As WndClassEx
Local szAppName As Asciiz*32
Local Msg As tagMsg
'
szAppName="Scrolling"
winclass.cbSize=SizeOf(winclass) :winclass.style=%CS_HREDRAW Or %CS_VREDRAW
winclass.lpfnWndProc=CodePtr(fnWndProc) :winclass.cbClsExtra=0
winclass.cbWndExtra=0 :winclass.hInstance=hIns
winclass.hIcon=LoadIcon(%NULL,ByVal %IDI_APPLICATION) :winclass.hCursor=LoadCursor(%NULL,ByVal %IDC_ARROW)
winclass.hbrBackground=GetStockObject(%WHITE_BRUSH) :winclass.lpszMenuName=%NULL
winclass.lpszClassName=VarPtr(szAppName) :winclass.hIconSm=0
Call RegisterClassEx(winclass)
dwStyle=%WS_MINIMIZEBOX Or %WS_VISIBLE Or %WS_VSCROLL Or %WS_SYSMENU
hMainWnd=CreateWindowEx(0,szAppName,szAppName,dwStyle,200,100,300,225,%HWND_DESKTOP,0,hIns,ByVal 0)
ShowWindow hMainWnd,iShow
UpdateWindow hMainWnd
While GetMessage(Msg,%NULL,0,0)
TranslateMessage Msg
DispatchMessage Msg
Wend
Function=msg.wParam
End Function
On my system the program revealed twelve full lines of text numbered 0 through 11 and about half of line # 12 was visible. Below is the Output.txt file created after clicking the Down arrow scroll bar button five times, and then closing the program...
Entering fnWndProc_OnCreate()
wea.iLinesVisible= 12
wea.iScrollRange= 14
wea.cyChar = 16
Leaving fnWndProc_OnCreate()
Entering fnWndProc_OnPaint()
wea.iBegin = 0
wea.iScrollRange = 14
wea.iLinesVisible = 12
ps.rcPaint.nTop = 0
ps.rcPaint.nBottom = 200
iStart = 0
iFinish = 12
i i*wea.cyChar iLine strLine(iLine)
======================================================================
0 0 0 0 PowerBASIC Scrolling Demo!
1 16 1 1 PowerBASIC Scrolling Demo!
2 32 2 2 PowerBASIC Scrolling Demo!
3 48 3 3 PowerBASIC Scrolling Demo!
4 64 4 4 PowerBASIC Scrolling Demo!
5 80 5 5 PowerBASIC Scrolling Demo!
6 96 6 6 PowerBASIC Scrolling Demo!
7 112 7 7 PowerBASIC Scrolling Demo!
8 128 8 8 PowerBASIC Scrolling Demo!
9 144 9 9 PowerBASIC Scrolling Demo!
10 160 10 10 PowerBASIC Scrolling Demo!
11 176 11 11 PowerBASIC Scrolling Demo!
12 192 12 12 PowerBASIC Scrolling Demo!
Leaving fnWndProc_OnPaint()
Entering fnWndProc_OnPaint()
wea.iBegin = 1
wea.iScrollRange = 14
wea.iLinesVisible = 12
ps.rcPaint.nTop = 184
ps.rcPaint.nBottom = 200
iStart = 11
iFinish = 12
i i*wea.cyChar iLine strLine(iLine)
======================================================================
11 176 12 12 PowerBASIC Scrolling Demo!
12 192 13 13 PowerBASIC Scrolling Demo!
Leaving fnWndProc_OnPaint()
Entering fnWndProc_OnPaint()
wea.iBegin = 2
wea.iScrollRange = 14
wea.iLinesVisible = 12
ps.rcPaint.nTop = 184
ps.rcPaint.nBottom = 200
iStart = 11
iFinish = 12
i i*wea.cyChar iLine strLine(iLine)
======================================================================
11 176 13 13 PowerBASIC Scrolling Demo!
12 192 14 14 PowerBASIC Scrolling Demo!
Leaving fnWndProc_OnPaint()
Entering fnWndProc_OnPaint()
wea.iBegin = 3
wea.iScrollRange = 14
wea.iLinesVisible = 12
Leaving fnWndProc_OnPaint()
Entering fnWndProc_OnPaint()
wea.iBegin = 4
wea.iScrollRange = 14
wea.iLinesVisible = 12
Leaving fnWndProc_OnPaint()
In the WM_CREATE message handler I determined that the height of a character in pixels was 16. Then we called GetClientRect() passing it the address of a RECT type. On my system the number 200 was extracted from the rc.nBottom member of the RECT variable rc. That number divided by the character height of 16 yields 12.5, but, being as I wanted to base my calculations on full lines, I used the PowerBASIC Fix() function to truncate the result of the division to 12. So this means that the integer 12 will be assigned to the wea.iLinesVisible variable. Note that the wea variable of type WndEventArgs is declared as static in fnWndProc() because many of its members must retain their values across function calls. The other calculations in the WM_CREATE event handler should be familiar to you from Simplest.bas. If not, you definitely need to reveiw that program. However, the wea.lpsi variable won't be familiar to you because we didn't use it in Simplest.bas. In terms of that variable, Microsoft amalgamated several of the variables related to scrolling into a type named SCROLLINFO in much the same manner as I amalgameted the Window Procedure parameters into the WndEventArgs Type. You would do well to open your Api documentation and famaliarize yourself with the SCROLLINFO type, as I use it in this program.
What I want to discuss now though is what you would have seen if you clicked the scroll down button several times as I suggested, and then opened that text file. What you should have seen is that for the first three clicks the window would have scrolled normally. Then disaster struck! With each click after that third the upper part of the screen scrolled up normally, but the last line at the bottom wasn't replaced by the next line in order 'beneath' the bottom edge of the window. Each click made it worse and expanded the vacant area. Continued clicks eventually cleared the whole window as it was 'scrolled away'. I did this on purpose to help you see what is going on, and shortly below when we get to fnWndProc_OnPaint() we'll fix it.
Take a look now at the first WM_PAINT message output in the Output.txt file above. Specifically, this...
Entering fnWndProc_OnPaint()
wea.iBegin = 0
wea.iScrollRange = 14
wea.iLinesVisible = 12
ps.rcPaint.nTop = 0
ps.rcPaint.nBottom = 200
iStart = 0
iFinish = 12
i i*wea.cyChar iLine strLine(iLine)
======================================================================
0 0 0 0 PowerBASIC Scrolling Demo!
1 16 1 1 PowerBASIC Scrolling Demo!
2 32 2 2 PowerBASIC Scrolling Demo!
3 48 3 3 PowerBASIC Scrolling Demo!
4 64 4 4 PowerBASIC Scrolling Demo!
5 80 5 5 PowerBASIC Scrolling Demo!
6 96 6 6 PowerBASIC Scrolling Demo!
7 112 7 7 PowerBASIC Scrolling Demo!
8 128 8 8 PowerBASIC Scrolling Demo!
9 144 9 9 PowerBASIC Scrolling Demo!
10 160 10 10 PowerBASIC Scrolling Demo!
11 176 11 11 PowerBASIC Scrolling Demo!
12 192 12 12 PowerBASIC Scrolling Demo!
Leaving fnWndProc_OnPaint()
This paint message was received right after the WM_CREATE message, and it was as a result of the processing within this message that the window became visible, showing, on my screen, 12 lines of text. Note that the PAINTSTRUCT variable ps has that RECT structure contained within it that contains the top and bottom coordinates of the invalid rectangle that needs to be painted. In our case the two numbers, i.e., 0 and 200, coincide exactly with the top and bottom of the client area. At this point this is no different from the initial situation with Simplest.bas. The whole window is invalid and the for loop will start drawing lines from the top to the bottom of the window.
Now take a look at the next call to fnWndProc_OnPaint(). This call would have been precipitated by your first click of the scroll down button....
Entering fnWndProc_OnPaint()
wea.iBegin = 1
wea.iScrollRange = 14
wea.iLinesVisible = 12
ps.rcPaint.nTop = 184
ps.rcPaint.nBottom = 200
iStart = 11
iFinish = 12
i i*wea.cyChar iLine strLine(iLine)
======================================================================
11 176 12 12 PowerBASIC Scrolling Demo!
12 192 13 13 PowerBASIC Scrolling Demo!
Leaving fnWndProc_OnPaint()
When you clicked that button for the first time, a WM_VSCROLL message would have been sent to the window procedure, and it would have showed up in fnWndProc_OnVScroll(). Since it would have contained an SB_LINEDOWN in its Lowrd(wea.wParam), the following call would have been made...
Call ScrollWindow(wea.hWnd,0,-wea.cyChar,ByVal 0,ByVal 0)
...and the text would have been scrolled up -cyChar or 16 pixels (on my system), which is the height of a line of text. That would have uncovered cyChar pixels at the bottom of the window, and so it would only be those 16 pixels from 184 to 200 that would have been invalid. In the output above you can see this is indeed what Windows reports to us in that rcPaint.nTop and rcPaint.nBottom variables. Please compare that with the first WM_PAINT output where the whole 200 pixel expanse of the window was invalid and in need of painting. If one were to try to describe in words just what Windows is doing with that ScrollWindow() call it would go something like this "When you call ScrollWindow(), that function will shift up or down, left or right, by the amount specified in its 2nd and 3rd parameters, whatever is presently visible on the screen. Anything shifted off the screen is lost, and any areas newly uncovered remain blank and 'invalid'. After that first scroll bar click, the bottom 16 pixels of the window from pixel 184 to 200 became invalid.
One other point I might make is that I purposely created the window so that the size of the client area was not an even multiple of the character height of wea.cyChar (16 on my system). So the last line visible is only half visible - the bottom half being off the display. That is why even though we only scrolled a distance of wea.cyChar, that distance involved the bottom half of one line and the top half of another. Since we can't TextOut() half lines, it was necessary to to print two whole lines. This will usually be the case. While you may initially set up a window all nice and proper with even multiples of line heights, diabolical users who aren't satisfied with your choices will resize the window and thereby create uneven configurations. So that's what's going on there.
Now lets take a look at fnWndProc_OnPaint(), which function is certainly implicated in all this...
Function fnWndProc_OnPaint(wea As WndEventArgs) As Long
Local iStart As Long, iFinish As Long,iLine As Long
Static iPainted As Long
Local ps As PAINTSTRUCT
Register i As Dword
Local hDC As Dword
'
Print #fp, "Entering fnWndProc_OnPaint()"
hDC=BeginPaint(wea.hWnd,ps)
Print #fp, "wea.iBegin = "wea.iBegin
Print #fp, "wea.iScrollRange = "wea.iScrollRange
Print #fp, "wea.iLinesVisible = "wea.iLinesVisible
If iPainted<3 Then
iStart=ps.rcPaint.nTop\wea.cyChar
iFinish=Ceil(ps.rcPaint.nBottom/wea.cyChar)-1
Print #fp, "ps.rcPaint.nTop = "ps.rcPaint.nTop
Print #fp, "ps.rcPaint.nBottom = "ps.rcPaint.nBottom
Print #fp, "iStart = "iStart
Print #fp, "iFinish = "iFinish
Print #fp,
Print #fp, "i i*wea.cyChar iLine strLine(iLine)"
Print #fp, "======================================================================"
For i=iStart To iFinish
iLine=wea.iBegin+i
If iLine<=%LAST_LINE Then
Print #fp, i,i*wea.cyChar,iLine,strLine(iLine)
Call TextOut(hDC,0,i*wea.cyChar,ByVal StrPtr(strLine(iLine)),Len(strLine(iLine)))
End If
Next i
Incr iPainted
End If
Call EndPaint(wea.hWnd,ps)
Print #fp,
Print #fp, "Leaving fnWndProc_OnPaint()"
Print #fp, : Print #fp, : Print #fp,
'
fnWndProc_OnPaint=0
End Function
The critical variables and code would be the iStart and iFinish variables, how they are initialized, and the for loop. If you recall back in Simplest.bas ( the brute force program ), if the Window contained 12 lines and you wanted to scroll down one line to see line 13, iStart would be incremented to two and iFinish to thirteen, an InvalidateRect() call would be made to force a repaint of the entire window, and the WM_PAINT handler would run a for loop for lines 2 through 13 so as to make line thirteen visible. The only time that happens here is on the initial painting of the window right after the WM_CREATE and initial WM_SIZE message. That is proved by the first set of output above where pixels 0 through 200 were invalid. In typical scrolling situations what happens is better described by the second WM_PAINT output above where only 16 pixels rather than 200 pixels in a verticle direction were invalidated. What happened in that case is that windows executed an internal and optimized GDI 'Raster Blaster' operation which shifted the upper part of the screen up by 16 pixels. iStart was initialized to the result of an integer division of ps.rcPaint.nTop against wea.cyChar and iFinish was handled similiarly with regard to the bottom of the invalid region and further corrected by some code wizzardry to nudge the number up to account for odd Window sizes and so forth. The end result of that code was that iStart was set to 11 and iFinish to 12. When the for loop executed these numbers multiplied by 16 to account for the size of a line in height, the end result was to get two lines at the bottom of the display rather than one line drawn. In actual practice this is usually how the numbers work out due to the fact that window sizes are seldom an exact multiple of character heights, and a small amount of extra drawing may be necessary, but it is certainly more efficient than redrawing the entire screen!
In terms of how I created the screw up where increasingly large areas of the screen became blank (so as to help you see how ScrollWindow() works), check out the iPainted variable in fnWndProc_OnPaint() above, which I declared as static. PowerBASIC would have set it to zero at program start, but each call to the WM_PAINT function would have incremented it by one. By looking at the If statement you can see that processing only reached the for loop that repainted that 16 pixel area at the bottom of the window for the first three scroll down messages. After that the ScrollWindow() call in fnWndProc_OnVScroll() indeed shifted the Window's contents up by a line height after each scroll message, but processing was hindered from entering the logic within the If statement in the WM_PAINT handler which calculates the necessary lines to repaint in terms of the invalid region sent to the procedure. So to repeat, I sabatoged the program on purpose with that iPainted variable so that it would only work properly for the first three scroll bar clicks. After that, the program intentionally malfunctions to show you what the ScrollWindow() function call does just by itself, and what it leaves up to you to complete.
For your own understanding of the processes involved here I would recommend you comment out the four lines which involve iPainted in the above fnWndProc_OnPaint() procedure, particelarly the If and End If statements, and rerun, study and experiment with the code. This particular example is particularly good because it is as close as we can get to Simplest.bas using this new high powered ScrollWindow() call in conjunction with SCROLLINFO. The reason I'm saying this is that the next iteration of this program below incorporates full scrolling functionality with resizeable borders, page up and page down processing, thumbtrack processing, keyboard processing, and so on. It is not really more difficult; just more involved. So I'd recommend you do your best to understand this simpler one before we move on.
One further point I might make. This material isn't easy. Don't expect to sit down and bang out your own perfectly working version of this in a few hours. A few days might be more like it. Maybe longer. To learn this material I'd guesstimate you'll need to generate 250 to 500 GPFs in your endeavors. If you really want to learn this material though, I'd recommend you try just that however. Start with a Hello, World template and start building the 'ediface' of a scrollable window from scratch, looking occasionally at these examples when you get stuck, until you begin to fully understand the issues involved. So, having said that lets augment ScrollWindow1.bas to ScrollWindow.bas (decrementing it actually) and incorporate these neat extra features I alluded to above. Here is ScrollWindow.bas...
#Compile Exe 'Uses ScrollWindow() in conjunction with SetScrollInfo().
#Dim All 'Don't even think about not using this!
#Include "Win32api.inc" 'You ought to spend some considerable time examining this file with Notepad.
Global strLine() As String 'Dynamic strings are nice. Unfortunately, you need to be a C or assembler
%LAST_LINE = 100 'programmer to really appreciate them!
%DEBUG = %FALSE
#If %DEBUG 'If you set %DEBUG to %TRUE, an output log file will be created and
Global fp As Long 'produce reams of output!
#EndIf
Type WndEventArgs 'Package frequently needed parameters
hIns As Dword 'Several of these variables need to be statics so as to retain Scroll
hWnd As Dword 'information between WndProc() Calls.
wParam As Dword
lParam As Dword
cyChar As Dword 'Verticle Size of a Character
iBegin As Long 'Offset into array of first line (array holds lines of text to scroll)
iScrollRange As Long 'iBegin to iEnd, as long as these values are in iScrollRange
iLinesVisible As Long 'One based count of lines visible on screen, adjusted as user resizes screen.
lpsi As SCROLLINFO 'Type / structure Microsoft now wishes us to use to set scroll position and
End Type 'scroll range
Function fnWndProc_OnCreate(wea As WndEventArgs) As Long
Local ps As PAINTSTRUCT 'Examine this in Win32Api.inc!!
Local tm As TEXTMETRIC 'Likewise with this!
Register i As Dword
Local hDC As Dword
#If %DEBUG
fp=Freefile
Open "Output.txt" For Output As #fp
Print #fp, "Entering fnWndProc_OnCreate()"
#EndIf
ReDim strLine(%LAST_LINE) 'Allocate memory buffer for lines of text to scroll
For i = 0 To UBound(strLine,1) 'Fill buffers with lines of text
strLine(i)=Str$(i)+" "+ _
"PowerBASIC Scrolling Demo!"
Next i
hDC=GetDC(wea.hWnd) 'In Windows, you need something called a 'Device Context' to get
Call GetTextMetrics(hDC,tm) 'info on how things such as text will be displayed. To find out
wea.cyChar=tm.tmHeight 'how many lines of text will be visible on any screen size chosen
Call ReleaseDC(wea.hWnd,hDC) 'by a user, we need the height of a character, 'wea.cyChar'.
wea.lpsi.cbSize=SizeOf(wea.lpsi) 'Api Help wants us to specify size of SCROLLINFO structure.
#If %DEBUG
Print #fp, "wea.iBegin = "wea.iBegin
Print #fp, "wea.iScrollRange = "wea.iScrollRange
Print #fp, "wea.iLinesVisible = "wea.iLinesVisible
Print #fp, "Leaving fnWndProc_OnCreate()"
Print #fp,
#EndIf
fnWndProc_OnCreate=0
End Function
Function fnWndProc_OnSize(wea As WndEventArgs) As Long
#If %DEBUG
Print #fp, "Entering fnWndProc_OnSize()"
#EndIf
wea.iLinesVisible=Fix(HiWrd(wea.lParam)/wea.cyChar) 'wea.lParam holds height of client area in pixels
wea.iScrollRange=%LAST_LINE-wea.iLinesVisible+1 'Last Line is #100, but that's a zero based count,
wea.lpsi.fMask=%SIF_ALL 'so add one to it (iLinesVisible is one based).
wea.lpsi.nMin=0 : wea.lpsi.nMax=wea.iScrollRange 'Set mins and maxes in lpsi (SCROLLINFO type)
wea.lpsi.nPos=wea.iBegin 'iBegin is zero based number of first line visible
Call SetScrollInfo(wea.hWnd,%SB_VERT,wea.lpsi,%TRUE) 'in window.
#If %DEBUG
Print #fp, "wea.iBegin = "wea.iBegin
Print #fp, "wea.iScrollRange = "wea.iScrollRange
Print #fp, "wea.iLinesVisible = "wea.iLinesVisible
Print #fp, "Leaving fnWndProc_OnSize()"
Print #fp,
#EndIf
fnWndProc_OnSize=0
End Function
Function fnWndProc_OnKeyDown(wea As WndEventArgs) As Long 'We already have code in place to handle scroll
Select Case wea.wParam 'line up, scroll line down, scroll page up, etc.
Case %VK_PGUP 'That being the case, there's no sense in
Call SendMessage(wea.hWnd,%WM_VSCROLL,%SB_PAGEUP,0) 'duplicating that code here. Since proper
Case %VK_UP 'message processing logic exists for these scroll
Call SendMessage(wea.hWnd,%WM_VSCROLL,%SB_LINEUP,0) 'messages, when keyboard messages are received
Case %VK_PGDN 'in here, we'll just send the corresponding
Call SendMessage(wea.hWnd,%WM_VSCROLL,%SB_PAGEDOWN,0)'scroll message to the Window Procedure directly.
Case %VK_DOWN
Call SendMessage(wea.hWnd,%WM_VSCROLL,%SB_LINEDOWN,0)
End Select
fnWndProc_OnKeyDown=0
End Function
Function fnWndProc_OnVScroll(wea As WndEventArgs) As Long
#If %DEBUG
Print #fp, "Entering fnWndProc_OnVScroll()"
#EndIf
Select Case LoWrd(wea.wParam)
Case %SB_LINEUP
If wea.iBegin Then 'Check to make sure not already at
Decr wea.iBegin 'strLine(0)
Call ScrollWindow(wea.hWnd,0,wea.cyChar,ByVal 0,ByVal 0) 'Bump everything 16 pixels (cyChar) down.
wea.lpsi.fMask=%SIF_POS
wea.lpsi.nPos=wea.iBegin
Call SetScrollInfo(wea.hWnd,%SB_VERT,wea.lpsi,%TRUE)
End If
#If %DEBUG
Print #fp, "Lowrd(wea.wParam) =SB_LINEUP"
#EndIf
Case %SB_PAGEUP
If wea.iBegin-wea.iLinesVisible >= 0 Then
wea.iBegin = wea.iBegin - wea.iLinesVisible
Call ScrollWindow(wea.hWnd,0,wea.iLinesVisible*wea.cyChar,ByVal 0,ByVal 0)
wea.lpsi.fMask=%SIF_POS 'The fMask member of lpsi prepares for the SetScrollInfo
wea.lpsi.nPos=wea.iBegin 'call and tells the function which members we are setting
Call SetScrollInfo(wea.hWnd,%SB_VERT,wea.lpsi,%TRUE)
Else
Call ScrollWindow(wea.hWnd,0,wea.iBegin*wea.cyChar,ByVal 0,ByVal 0)
wea.iBegin=0
wea.lpsi.fMask=%SIF_POS
wea.lpsi.nPos=wea.iBegin
Call SetScrollInfo(wea.hWnd,%SB_VERT,wea.lpsi,%TRUE)
End If
#If %DEBUG
Print #fp, "Lowrd(wea.wParam) =SB_PAGEDOWN"
#EndIf
Case %SB_LINEDOWN
If wea.iBegin+wea.iLinesVisible<=%LAST_LINE Then
Incr wea.iBegin
Call ScrollWindow(wea.hWnd,0,-wea.cyChar,ByVal 0,ByVal 0)
wea.lpsi.fMask=%SIF_POS
wea.lpsi.nPos=wea.iBegin
Call SetScrollInfo(wea.hWnd,%SB_VERT,wea.lpsi,%TRUE)
End If
#If %DEBUG
Print #fp, "Lowrd(wea.wParam) =SB_LINEDOWN"
#EndIf
Case %SB_PAGEDOWN
If wea.iBegin+wea.iLinesVisible<=%LAST_LINE Then
wea.iBegin = wea.iBegin + wea.iLinesVisible
Call ScrollWindow(wea.hWnd,0,-wea.iLinesVisible*wea.cyChar,ByVal 0,ByVal 0)
wea.lpsi.fMask=%SIF_POS
wea.lpsi.nPos=wea.iBegin
Call SetScrollInfo(wea.hWnd,%SB_VERT,wea.lpsi,%TRUE)
End If
#If %DEBUG
Print #fp, "Lowrd(wea.wParam) =SB_PAGEDOWN"
#EndIf
Case %SB_THUMBTRACK
wea.lpsi.fMask=%SIF_TRACKPOS
Call GetScrollInfo(wea.hWnd,%SB_VERT,wea.lpsi)
wea.iBegin=wea.lpsi.nTrackPos
Call InvalidateRect(wea.hWnd,ByVal 0,%TRUE)
wea.lpsi.fMask=%SIF_POS
wea.lpsi.nPos=wea.lpsi.nTrackPos
Call SetScrollInfo(wea.hWnd,%SB_VERT,wea.lpsi,%TRUE)
#If %DEBUG
Print #fp, "Lowrd(wea.wParam) =SB_THUMBTRACK"
#EndIf
End Select
#If %DEBUG
Print #fp, "wea.iBegin = "wea.iBegin
Print #fp, "wea.iScrollRange = "wea.iScrollRange
Print #fp, "wea.iLinesVisible = "wea.iLinesVisible
Print #fp, "Leaving fnWndProc_OnVScroll()"
Print #fp,
#EndIf
fnWndProc_OnVScroll=0
End Function
Function fnWndProc_OnPaint(wea As WndEventArgs) As Long
Local iStart As Long, iFinish As Long,iLine As Long
Local ps As PAINTSTRUCT
Register i As Dword
Local hDC As Dword
#If %DEBUG
Print #fp, "Entering fnWndProc_OnPaint()"
#EndIf
hDC=BeginPaint(wea.hWnd,ps)
#If %DEBUG
Print #fp, "wea.iBegin = "wea.iBegin
Print #fp, "wea.iScrollRange = "wea.iScrollRange
Print #fp, "wea.iLinesVisible = "wea.iLinesVisible
#EndIf
iStart=ps.rcPaint.nTop\wea.cyChar
iFinish=Ceil(ps.rcPaint.nBottom/wea.cyChar)-1
#If %DEBUG
Print #fp, "iStart = "iStart
Print #fp, "iFinish = "iFinish
Print #fp,
Print #fp, "i i*wea.cyChar iLine strLine(iLine)"
Print #fp, "======================================================================"
#EndIf
For i=iStart To iFinish
iLine=wea.iBegin+i
If iLine<=%LAST_LINE Then
#If %DEBUG
Print #fp, i,i*wea.cyChar,iLine,strLine(iLine)
#EndIf
Call TextOut(hDC,0,i*wea.cyChar,ByVal StrPtr(strLine(iLine)),Len(strLine(iLine)))
End If
Next i
Call EndPaint(wea.hWnd,ps)
#If %DEBUG
Print #fp,
Print #fp, "Leaving fnWndProc_OnPaint()"
Print #fp, : Print #fp, : Print #fp,
#EndIf
fnWndProc_OnPaint=0
End Function
Function fnWndProc_OnClose(wea As WndEventArgs) As Long
#If %DEBUG
Close #fp
#EndIf
Erase strLine
Call PostQuitMessage(0)
fnWndProc_OnClose=0
End Function
Function fnWndProc(ByVal hWnd As Long,ByVal wMsg As Long,ByVal wParam As Long,ByVal lParam As Long) As Long
Static wea As WndEventArgs
Select Case wMsg
Case %WM_CREATE
wea.hWnd=hWnd: wea.wParam=wParam: wea.lParam=lParam
fnWndProc=fnWndProc_OnCreate(wea)
Exit Function
Case %WM_SIZE
wea.hWnd=hWnd: wea.wParam=wParam: wea.lParam=lParam
fnWndProc=fnWndProc_OnSize(wea)
Exit Function
Case %WM_KEYDOWN
wea.hWnd=hWnd: wea.wParam=wParam: wea.lParam=lParam
fnWndProc=fnWndProc_OnKeyDown(wea)
Exit Function
Case %WM_VSCROLL
wea.hWnd=hWnd: wea.wParam=wParam: wea.lParam=lParam
fnWndProc=fnWndProc_OnVScroll(wea)
Exit Function
Case %WM_PAINT
wea.hWnd=hWnd: wea.wParam=wParam: wea.lParam=lParam
fnWndProc=fnWndProc_OnPaint(wea)
Exit Function
Case %WM_CLOSE
wea.hWnd=hWnd: wea.wParam=wParam: wea.lParam=lParam
fnWndProc=fnWndProc_OnClose(wea)
Exit Function
End Select
fnWndProc=DefWindowProc(hWnd,wMsg,wParam,lParam)
End Function
Function WinMain(ByVal hIns As Long, ByVal hPrev As Long, ByVal lpCmdLn As Asciiz Ptr, ByVal iShow As Long) As Long
Local hMainWnd As Dword,dwStyle As Dword
Local winclass As WndClassEx
Local szAppName As Asciiz*16
Local Msg As tagMsg
szAppName="Scroll Window"
winclass.cbSize=SizeOf(winclass) :winclass.style=%CS_HREDRAW Or %CS_VREDRAW
winclass.lpfnWndProc=CodePtr(fnWndProc) :winclass.cbClsExtra=0
winclass.cbWndExtra=0 :winclass.hInstance=hIns
winclass.hIcon=LoadIcon(%NULL,ByVal %IDI_APPLICATION) :winclass.hCursor=LoadCursor(%NULL,ByVal %IDC_ARROW)
winclass.hbrBackground=GetStockObject(%WHITE_BRUSH) :winclass.lpszMenuName=%NULL
winclass.lpszClassName=VarPtr(szAppName) :winclass.hIconSm=0
Call RegisterClassEx(winclass)
dwStyle=%WS_THICKFRAME Or %WS_MINIMIZEBOX Or %WS_VISIBLE Or %WS_VSCROLL Or %WS_SYSMENU
hMainWnd=CreateWindowEx(0,szAppName,szAppName,dwStyle,200,100,300,228,%HWND_DESKTOP,0,hIns,ByVal 0)
Call ShowWindow(hMainWnd,iShow)
Call UpdateWindow(hMainWnd)
While GetMessage(Msg,%NULL,0,0)
Call TranslateMessage(Msg)
Call DispatchMessage(Msg)
Wend
Function=msg.wParam
End Function
.....continued next post...
...continued...
You will note that a #DEBUG conditional compilation statement exists near the top of the file. If %DEBUG is defined as %TRUE an Output.txt log file will be created for you in the directory in which the program is running. If it is %FALSE there will be no file created. If you havn't run this program yet do so now. The extra features incorporated into this program are as follows:
1) Window is resizable through dragging window borders or minimize and maximize button;
2) Scroll Page Up and Scroll Page Down were implemented in the typical way by clicking in the scroll bar track;
3) Thumbtrack processing was enabled for dragging the scroll bar thumb;
4) A keyboard interface using the cursor up, cursor down, Page Up, and Page Down keys was implemented.
The key to many of these features was the addition of a %WM_SIZE message handler - fnWndProc_OnSize. If you examine the %WM_CREATE processing in the first program - ScrollWindow1.bas, you will see that in that program the size of the program's window was determined by a call to GetClientRect(). The value returned by that call remained constant for the duration of that program's run time life because when the CreateWindow() call was made in WinMain() that created the window, I used the following style variable in the call...
dwStyle=%WS_MINIMIZEBOX Or %WS_VISIBLE Or %WS_VSCROLL Or %WS_SYSMENU
This style is such that it will create a window that can't be resized. In the latter program - ScrollWindow.bas I used this combination of styles...
dwStyle=%WS_THICKFRAME Or %WS_MINIMIZEBOX Or %WS_VISIBLE Or %WS_VSCROLL Or %WS_SYSMENU
You may note the addition of the %WS_THICKFRAME window style. This will cause the window to have a resizable border. To understand the effect resizing has on the scrolling of a window you have to consider how the values returned by that GetClientRect() call were used. We needed them to determine the count of visible lines on the screen. This count is of course necessary because when the user wishes to scroll up or down, we need to know how many lines need to be scrolled. Here is the pertinent line of code from that WM_CREATE handler of ScrollWindow1.bas...
Local rc As RECT
Call GetClientRect(wea.hWnd,rc)
wea.iLinesVisible=Fix(rc.nBottom/wea.cyChar)
Clearly, we divided the value obtained from the bottom member of the rc structure/type with the character height to get the number of lines of text visible on the screen. Since the window couldn't be resized, once that value was set upon window creation, it remained constant for the duration of the program.
Well, if we are going to allow the user to resize the window, that number isn't going to be a constant anymore. So how do you determine it?
If you recall back in the second post in this Api tutorial there is a program named Form2.bas. That program had resizable borders and displayed in the client area of the window the window height and width as the borders were resized. Do you remember how we did that? What we did was put a %WM_SIZE message handler in the program and obtained in that the dimensions of the client window through the parameters passed to us in the window procedure. Specifically, the window's client area height is obtained through the high order 16 bit word of the 32 bit lParam parameter, i.e.,
wea.iLinesVisible = Fix( Hiwrd(wea.lParam) / wea.cyChar ).
Once you have obtained the new size of the window, all that needs to be done then is recalculate a new Scroll Range like so...
wea.iScrollRange=%LAST_LINE-wea.iLinesVisible+1
If you are having any trouble with the concept of 'Scroll Range', perhaps thinking about it like as follows will help. Say for example you have a hundred lines of text numbered 1 to 100. Further, say you have a window that shows only 10 lines of text, which, upon program start up are visible as lines 1 through 10. How many 'clicks' of the scroll bar are needed then to see the additional 90 lines of text in a buffer containing 100 lines? Surely not 100 if you can already see ten of them! The answer is of course 90. That is all that is really going on with the concept of Scroll Range. The only other factor is that the minimum scroll position and the maximum scroll position must be set, and it isn't hard at all to manage to be off by one due to 'fence post' errors.
In ScrollWindow.bas the last subscript of the array strLine() is 100. Since the zeroeth element was used that makes for 101 elements. Since iLinesVisible is a 1 based count we add 1 to the difference to come up with the zero based scroll range. This is then set in the SCROLLINFO TYPE with SetScrollInfo(). Don't worry too much about the numbers. Sometimes they have to be 'juggled' to make it work right!
That's really just about it! I'd recommend highly that you experiment with the program by doing a very limited number of things to it, then opening the Output.txt debug log file to study the results of your actions. I'm saying to do very few things because of the furious activity generated by nearly anything you do, especially sizing, page up - page -down, or thumbtrack events. Just a few seconds of any of those activities will result in an output file over a megabyte - and that is far too much information to understand or sort out. Good Luck!