Fred's Tutorial #9: Scrolling Part Two - ScrollWindow.Bas

Started by Frederick J. Harris, September 11, 2007, 04:37:58 AM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

Frederick J. Harris

 
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...


Frederick J. Harris

     ...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!