---------------------------------------------------------------------- T h e Q B N e w s P r o f e s s i o n a l L i b r a r y ---------------------------------------------------------------------- Popup Windows by Christy Gemmel By now, you should have a good idea of how assembly-language can be used with your QuickBASIC programs. The routines we've developed for our library, so far, are short and simple, but they've already given you a lot more power over your mouse and the video display. Hopefully your appetite is whetted for more. Well now we're starting on the heavy stuff. What I have for you next is a complete Window Management System. Windows can be popped up at any location on the screen, overlayering each other if you require. They can be displayed in any combination of colours or attributes and come in a variety of border styles. They can have shadow, to give a three-dimensional effect, and you can zoom them onto the screen for a really slick effect... What are Windows? If you've used the QuickBASIC environment at all, then you've used Windows. When you press to bring down the File Menu, the list of options presented there is in a window. Notice how any characters which were hidden when the menu appeared, are restored, intact, after you've made your choice and the menu window vanishes. Windows are areas of the screen which are used to hold transient data and messages to the user. They make the most of the limited display space available and remove the need to be constantly redrawing the screen each time your program communicates with the outside world. Properly presented, windows can give the illusion of multi-tasking, even on a single-processor machine like your PC. Nowadays, no program worth its' salt can be without a window of some kind. If YOUR program is going to stand out amongst all the others, however, they've got to be done professionally. Your windows must appear instantly and vanish, just as quickly, when no longer needed. They must be as large or as small as is necessary, for the data which you need to display, and you should have a plentiful supply, enough for all the possible circumstances that your program might encounter. High-level languages, unfortunately, are just not fast enough to meet all these requirements. Looks like it'll have to be assembler again ... I'll start by explaining how windows work. You've already seen with FASTPRINT, how large amounts of text can be output directly to the video display in a flash. That bit shouldn't be any bother to us now. For the rest, the main problem seems to be how to restore the original screen contents when we take the window away, especially if we have windows overlapping on the same portion of the screen. The way to do it is this. First we establish a buffer in memory, large enough to hold the total contents of all the windows we plan to have on the screen at any one time. Then, just before we pop up a The QBNews Page 6 Volume 2, Number 4 December 22, 1991 window, we must copy the contents of the screen rectangle which will be covered by that window, into the buffer we have reserved. This way, when we have have finished with the window, all we have to do is to copy the original data from the buffer, back into the original rectangle it came from. Sounds easy? Well it is. The trick is in keeping track of which lot of buffer data corresponds to which window on the screen and where to put it all when we put it back. The rest is just byte shifting. How big does the buffer need to be? It depends on the both the number and the size of the windows which we plan to use. Remember that each character displayed on the screen takes up two bytes of information, one for the ASCII character code and the other for its' associated attribute. So a window 20 columns wide by 12 rows high would need (20 x 12) * 2 = 480 bytes of storage to hold the information under it. Our window driver will contain 16K of internal storage, the equivalent of 4 full screens, less a small amount of overhead, enough for most reasonable applications. To make things really easy, we'll operate the buffer on the stack principle, so that the most recently displayed window is always the first one to be removed. This is known as the LIFO method (Last In, First Out), and it prevents the possibility of gaps appearing in our buffer as windows are removed in different sequence from the one in which they were created. You wouldn't believe the memory management problems THAT can cause! What calling conventions should we use? We'll need to define the screen rectangle which the window will occupy, also the display attribute, since we want to pop up different coloured windows for different types of messages. How about border style? In the PANEL subprogram we used two types of border, single and double lines, perhaps we should offer a wider choice this time, here are some possible permutations. ÚÄÄÄÄ¿ ³ 1. ³ Single-lined box all round the window ÀÄÄÄÄÙ ÉÍÍÍÍ» º 2. º Double-lined box all round the window ÈÍÍÍͼ ÕÍÍÍ͸ ³ 3. ³ Single vertical, double horizontal ÔÍÍÍ; ÖÄÄÄÄ· º 4. º Single horizontal, double vertical ÓÄÄÄĽ An argument of zero can be used for plain windows, without borders. They might be useful sometimes. Is that the lot? Wait a minute though, a common application of windows is for Pull Down Menus, like the QuickBASIC File Menu which The QBNews Page 7 Volume 2, Number 4 December 22, 1991 you get by pressing . If we're going to cater for this sort of thing we need to include border styles which merge into a menu bar at the top, these for instance; ÍÍÑÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÑÍÍ ÍÍËÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍËÍÍ ³ ³ º º ³ ³ º º ³ 5. ³ º 6. º ³ ³ º º ³ ³ º º ÔÍÍÍÍÍÍÍÍÍÍÍÍÍÍ; ÈÍÍÍÍÍÍÍÍÍÍÍÍÍÍͼ ÄÄÂÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÂÄÄ ÄÄÒÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÒÄÄ ³ ³ º º ³ ³ º º ³ 7. ³ º 8. º ³ ³ º º ³ ³ º º ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÓÄÄÄÄÄÄÄÄÄÄÄÄÄÄĽ The SHADOW switch (Parameter 7), will be used to add a black shadow underneath the window, Giving it a three dimensional effect. Setting P7 to 1, puts the shadow on the left-hand side. Setting P7 to 2 puts it on the right. Any other value prevents shadow. ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÖÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ· Û³ ³ º ºÛ Û³ ³ º ºÛ Û³ Left Shadow ³ º Right Shadow ºÛ Û³ ³ º ºÛ Û³ ³ º ºÛ ÛÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÓÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ½Û ßßßßßßßßßßßßßßßßß ßßßßßßßßßßßßßßßßß Setting Parameter 8 to a non-zero value will cause the window to ZOOM onto the screen. What this means is that, starting at a point source, successively larger versions of the window will be drawn until it is the size required. The process is extremely fast and impressive, and will add a very professional touch to your programs. Like all assembly-language routines linked to QuickBASIC programs, our window code must use the Medium memory model. We will also be calling a number of external routines which are listed here. Some of these you will recognise from the FASTPRINT article. Source code for the others is provided with this issue. .model medium extrn Delay:proc extrn Explode:proc extrn ScreenAddress:proc extrn ScreenCopy:proc extrn ScreenWrite:proc extrn VideoType:proc extrn WriteByte:proc The QBNews Page 8 Volume 2, Number 4 December 22, 1991 The program will consist of two seperate modules, one is to draw the window on the screen and the other to remove it when it is no longer required. Both routines will be called from QuickBASIC, so they must be declared PUBLIC. public PopUp, ShutUp .code This program uses quite a lot of internal data. Let's declare it now and get it over with. For now, notice the buffer used for holding screen data, I have set this to 16KB (4000 Hex bytes) to give us room for lots of windows. If you find that this is too big (or too small), set it to your own value. No other changes are necessary. Ulc label word ; Upper left co-ordinate TlRow db ? ; Top left screen row TlCol db ? ; Top left screen column Lrc label word ; Lower right co-ordinate BrCol db ? ; Right column of window BrRow db ? ; Bottom row of window Area label word Breadth db ? ; Window width (inc shadow) Height db ? ; Window Height (inc shadow) ToDo label word Cols2do db ? ; Columns to restore Rows2do db ? ; Rows to restore Rows db ? ; Screen length in rows Columns db ? ; Screen width in columns Increment dw ? ; Interval between rows BuffPtr dw ? ; Pointer to current buffer BuffTop dw ? ; Offset of first buffer row BuffEnd dw ? ; Offset of last buffer row WinTop dw ? ; Offset of first screen row WinEnd dw ? ; Offset of last screen row TopLeft label byte db ' ÚÉÕÖÑËÂÒ' ; TL Corner characters TopRight label byte db ' ¿»¸·ÑËÂÒ' ; TR Corner characters BotLeft label byte db ' ÀÈÔÓÔÈÀÓ' ; BL Corner characters BotRight label byte db ' Ù¼¾½¾¼Ù½' ; BR Corner characters Vertical label byte db ' ³º³º³º³º' ; Vertical characters Horizontal label byte db ' ÄÍÍÄÍÍÄÄ' ; Horizontal characters Buffer label byte ; Start of screen buffer db 4000h dup(0) BufferTop dw 0 ; End of screen Buffer The QBNews Page 9 Volume 2, Number 4 December 22, 1991 Here's our introduction with BP being set, as usual, to point to the stack. Once we've used it to obtain all the parameters, we are going to point DS to our own local data. We need to save its' original contents, therefore, so that they can be reset on return. ES will be used to point to video memory so we'd better save that as well, likewise the two Index registers. PopUp proc far push bp ; Save Base pointer mov bp,sp ; Establish stack frame push ds ; Preserve segment push es ; registers and push di ; index push si ; pointers You will remember this call from the FASTPRINT article. VIDEOTYPE is a routine which we wrote to collect important information about the kind of video display the host computer has. In this case we used it to determine the number of rows and columns the screen is set to. The routine also found the address of the video display buffer and number of the video status port and has stored this information, internally, in its' own module. We will need it later. call VideoType ; Get video parameters POPUP and SHUTUP, The two routines in this module, do not use string data as arguments so we do not need to write a seperate version for BASIC 7 using far strings. All the parameters that we will be using are passed BY VALUE on the stack, so we do not need to keep our DS register pointing to QuickBASIC's Data Segment (DGROUP). Let's point it to our own local data so that we can access it more easily. push cs ; Align Code and pop ds ; Data segments Many QuickBASIC programmers who are new to Assembly-language assume that the DS register must be kept pointing to DGROUP while arguments are read off the stack. This is only true when it is the address of arguments which are passed, by reference, (QuickBASIC's default) and the routine must go looking in DGROUP for the variables themselves. Values read directly from stock, using the BP register as a pointer, are indexed through the SS (Stack Segment) register, so DS can be pointing elsewhere. We will store the data that VIDOTYPE collected, locally, in our own code segment. This saves using any of QuickBASIC's own data space which, even if you use BASIC 7 is always at a premium. Aligning the Code and Data registers allows us to store variables here without having to use nasty segment override instructions and reduces overhead considerably. mov Rows,bl ; Store screen height mov Columns,ah ; Store screen width mov al,ah ; Transfer number of The QBNews Page 10 Volume 2, Number 4 December 22, 1991 xor ah,ah ; columns to AX shl ax,1 ; and convert mov Increment,ax ; to bytes While we have the screen width handy in AH, we may as well take the opportunity to calculate the increment between rows for when we start drawing vertical lines. Since there are two bytes per column, one each for the character and it's display attribute, we must multiply the number of columns by two. Shifting each bit of the number to the left with SHL does this just as effectively and much faster than the MUL instruction and saves having to use another register. We've some chores to do before we can start on the interesting bits, Just as we did in FASTPRINT, we must first examine the four arguments which define the rectangle over which the window will appear, testing them for legal values; mov al,[bp+20] ; Get top-left row dec al ; Make it base zero cmp al,0 ; Check for jge Pop_01 ; legal xor al,al ; values Pop_01: mov TlRow,al ; Save top-left row mov al,[bp+18] ; Get top-left column dec al ; Make it base zero cmp al,0 ; Check for ja Pop_02 ; legal mov al,1 ; values Pop_02: mov TlCol,al ; Store top-left column mov al,[bp+16] ; Get window height cmp al,2 ; Check for ja Pop_03 ; legal mov al,3 ; values Pop_03: mov [bp+16],al ; Store window height mov al,[bp+14] ; Get window width cmp al,2 ; Check for ja Pop_04 ; legal mov al,3 ; values Pop_04: mov [bp+14],al ; Store window width mov al,TlRow ; Get start row mov ah,[bp+16] ; Get number of rows add al,ah ; Add 'em together cmp al,Rows ; Out of bounds? jb Pop_05 ; No, carry on jmp Pop_38 ; Else abort Pop_05: dec al ; Store bottom mov BrRow,al ; row number mov al,TlCol ; Get start column mov ah,[bp+14] ; Get number of columns The QBNews Page 11 Volume 2, Number 4 December 22, 1991 add al,ah ; Add 'em together cmp al,Columns ; Out of bounds? jb Pop_06 ; No, carry on jmp Pop_38 ; Else abort Pop_06: dec al ; Store rightmost mov BrCol,al ; column number Unlike the first two, the second pair of arguments do not give us the coordinates of the lower-right corner directly. To make it easier for our users, we'll accept the HEIGHT of the window (in rows) and the WIDTH of the window (in columns), instead. We must make a couple of small calculations to ensure that the window fits within the borders of the screen, not forgetting to allow for shadow. There are more parameters to come. The attribute value doesn't need to be checked, since we will only be using the least-significant byte of the integer value passed and all possible values (0-255) that this can contain are legal. From here on, we won't abort if an illegal argument is passed to us, instead we'll use a default. The default for BORDER type is 1, a single-lined border. mov al,[bp+10] ; Get required border type cmp al,0 ; Check jb Pop_07 ; for cmp al,8 ; legal ja Pop_07 ; values jmp short Pop_08 Pop_07: mov byte ptr [bp+10],1 ; Set default (single line) Pop_08: mov al,[bp+8] ; See if shadow is required cmp al,0 ; Check jl Pop_09 ; for cmp al,4 ; legal ja Pop_09 ; values jmp short Pop_10 Pop_09: mov byte ptr [bp+8],0 ; Set default (no shadow) Pop_10: cmp word ptr [bp+6],0 ; Check for jge Pop_11 ; legal mov word ptr [bp+6],20 ; values The default for SHADOW is zero - no shadow. With ZOOM, the argument is now used as a delay counter in milliseconds. Previous versions of this routine treated any non-zero value as logical TRUE and generated a fixed delay count. To cater for programs which still expect this, I have translated any negative ZOOM values (eg -1) into a delay of 20, which approximates to the previous ZOOM speed. This, incidently, is an important point to bear in mind when you are writing libraries for commercial (or Shareware) release. Whenever you update an existing routine, remember that your current users may have The QBNews Page 12 Volume 2, Number 4 December 22, 1991 to convert their old programs before they can use them with your new version. If you can make this job as painless as possible they will bless you for it. Pop_11: mov ax,1 ; Initialise push ax ; millisecond call Delay ; delay routine ZOOM, the speed at which the window will explode onto the screen, is controlled by the millisecond delay value we are passed. The smaller this number, the faster the explosion. We need, however to initialise the actual delay routine which is in our other module, DISPLAY.ASM. Since initialisation, itself, takes a little time it is best to do it here, where it won't be noticed, rather than add its' overhead to the first appearance of our window. Some more calculation is necessary, to obtain the co-ordinates of the window. If it is going to have shadow then an extra row and column of screen data will have to be saved in the buffer. Better work out the area of the window, so we can check if there is enough space left in the buffer. Multiply the number of rows in AX by the number of columns in BX, this leaves the product in AX. Double AX again to account for attribute bytes and transfer the result to CX. xor ax,ax ; Get number of rows mov al,[bp+16] ; into AX xor bx,bx ; Get number of columns mov bl,[bp+14] ; into BX cmp byte ptr [bp+8],0 ; Shadow required? jz Pop_12 ; No, skip next bit inc al ; Add a row inc bl ; and a column Pop_12: mov Height,al ; Store adjusted height mov Breadth,bl ; Store adjusted width mul bl ; Find area shl ax,1 ; in bytes mov cx,ax ; Transfer to CX mov si,offset Buffer ; Start of screen stack That big, huh? Let's see if it will fit. Perhaps I should explain how the buffer is organised. Since each block of screen data stored in it is likely to be of a different size, it is necessary to use the concept of a LINKED LIST. By this method, every block in the buffer begins with a pointer to the next block. In searching for a free block, we follow the chain along the list until we find an empty pointer. Any space following this should be available for use. Note that, initially, the buffer area was set to zeroes. In addition to the next-block pointer, we will store the offset address of the top left corner of each window in the buffer. The The QBNews Page 13 Volume 2, Number 4 December 22, 1991 height of the window, in rows, and it's width, in columns (including any extra allowed for shadow), will also be stored. This should be enough to specify the screen rectangles we need to save. Every block of buffer storage, therefore, will contain the following data ... width height ³ ³ ÚÄÄÄÄÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÄÄÄÄÂÄÄÄÄÄÂÄÄÄÄÄÂÄÄÄÄÄÄ ÄÄÄÄÄÄ¿ ³ 2 bytes ³ 2 bytes ³ 2 bytes ³ Variable length ³ ÀÄÄÄÄÄÄÄÄÄÄÄÁÄÄÄÄÄÄÄÄÄÄÄÁÄÄÄÄÄÁÄÄÄÄÄÁÄÄÄÄÄÄ ÄÄÄÄÄÄÙ ³ ³ ³ ³ Pointer to Offset of Dimensions Storage for screen data next block UL Corner of window of window (including any shadow) The SI register is now pointing to the start of the buffer, so we can examine the first two bytes right away. If they are empty (set to zero), then the whole buffer is empty and we can begin storing our block of screen data at once. If however, the first two bytes contain data, then we can assume that it is a pointer to the beginning of the NEXT block, so we load SI with this address and repeat the process. Only when SI points to a word of zeroes, do we we have free space to store our screen. Pop_13: cmp word ptr [si],0 ; Is anything there? jz Pop_14 ; No, must be free space mov si,[si] ; Point to next block jmp Pop_13 ; and try again Pop_14: mov ax,si ; Point AX to entry add ax,6 ; Allow for pointers add ax,cx ; and area to be saved mov dx,offset BufferTop ; Point DX to end of stack cmp ax,dx ; Enough space left? jb Pop_15 ; Yes, Carry on jmp Pop_38 ; Otherwise abort Before we begin copying data to the buffer, we must make sure that there is enough space left to hold it, for all we know there may be other windows up on the screen. We have already calculated the size of the block we need to save. Add another six bytes for our own pointers, then add the result to the offset address of the current block. If the resulting address is past the end of the buffer, then there is not enough room for the new window and there is no point in proceeding any further. If there is room for another window, then we can go ahead. Since AX, as a result of the last calculation, is already holding the address of the first byte past our save area, we can store this immediately, as the next-block-pointer. Pop_15: The QBNews Page 14 Volume 2, Number 4 December 22, 1991 mov [si],ax ; Set pointer to next block SCREENADDRESS is one of the support routines we developed for our FASTPRINT utility two issues back. We used it to calculate the screen address of the position where printing was to start. This time we are looking for the address of the top-left corner of the rectangle which will be covered by our window. If the window is going to have shadow on the left, we must also save an extra column on the left-hand side. mov ax,Ulc ; Get row-column co-ordinates call ScreenAddress ; Convert to memory address mov WinTop,di ; Save it for later test byte ptr [bp+8],1 ; Left shadow? jz Pop_16 ; No, skip next bit dec di ; One column dec di ; to the left Pop_16: inc si ; Bump buffer inc si ; pointer mov [si],di ; Store it in buffer inc si ; Bump buffer inc si ; pointer mov ax,Area ; Get panel Area mov [si],ax ; Store them in the buffer inc si ; Bump pointer to inc si ; screen storage block Once we have the address, it is stored, in the save buffer, at the word following the next-block-pointer. Finally, the third word in the save buffer is loaded with the height and width of the rectangle we are going to save. It is worth drawing your attention to the method we just used to access the dimensions of the rectangle. When we originally stored the height and width after they were calculated, we saved them as BYTE values. Now we come to use them we are able to copy them both, as a single WORD, into AX with one instruction. One of the nice features of MASM is that it allows us to define the same block of memory in several different ways: Area label word Breadth db ? ; Window width (inc shadow) Height db ? ; Window Height (inc shadow) In this example, BREADTH and HEIGHT are labels which define single bytes, but AREA, which also refers to the same location, is of size WORD (2 bytes). By using this as a reference, we can load both values directly into AX in one move, instead of the two which would be necessary if we had to load AH and AL individually. If you have stuck with me, through the examples in these articles, so far, you will have realised that most of the donkey-work of assembly- language programming, lies in setting up registers and data ready for some operation which, in itself, only takes a few instructions. Such The QBNews Page 15 Volume 2, Number 4 December 22, 1991 is the case here. We are going to use a nested loop to succesively copy each column of each row of the screen rectangle which our window will cover, into the memory buffer. As well as being one of the four 8086 general-purpose registers, CX can also be used as a loop counter. Our outer processing loop will govern the number of rows copied. This value is already in AH. Let's begin by copying it to CX. xor cx,cx ; Get number of rows mov cl,ah ; in CX Before we start, we must swap over the pointers we have been using so far. Currently the Data Segment register (DS) is pointing to the Code Segment where our local data is. Now, however, we need to treat the video display buffer as our Data segment because we are going to copy that part of the screen which will lie underneath our window into the buffer for safekeeping. xchg di,si ; Swap pointers push ds ; Point DS to push es ; video segment pop ds ; and ES to pop es ; local data The inner loop controls the number of columns to be copied. We'll use CX as the counter for this as well. Save the row count on the stack, temporarily, while we load the column count into CX. Remember this number includes an extra column for shadow, if specified. We'll also push SI, which contains the screen starting address, for reasons which will become clear. Pop_17: push cx ; Save row count push si ; Save screen pointer mov cl,cs:Breadth ; Set column count Now we can start moving data. SI is currently pointing to the address in video memory, where the first character to be moved is located. DS has the segment address of video memory. DI is pointing to the start of the first free block in our storage buffer, relative to the ES register. Since the actual shifting of bytes is a task that is likely to need doing on other occasions, we have dedicated a special routine to it, SCREENCOPY. This is listed in the new version of DISPLAY.ASM which is provided with issue. Pop_18: call ScreenCopy ; Copy word from screen loop Pop_18 ; For length of row The LOOP instruction decrements the value in CX and then returns control to the statement following the indicated label. It does this until the value of CX is reduced to zero, after which the program falls through to the next line. Since CX was originally loaded with The QBNews Page 16 Volume 2, Number 4 December 22, 1991 the number of columns in the row, this only happens when a full row has been copied. Back in the outer loop, we retrieve the original screen pointer from the stack and add the row increment value which we calculated earlier to it. This value may be 80, 160 or 264 bytes, depending upon whether our screen is set to 40, 80 or 132 columns, and will make SI point to the start of the next row to be moved. We also retrieve the row count into CX, so that the second LOOP instruction can be used to repeat the whole procedure until every row has been done. pop si ; Bump pointer add si,cs:Increment ; to next row pop cx ; Recover row count loop Pop_17 ; For each row That's the first stage complete. We have copied the whole of the rectangle which our window will cover, into the storage buffer reserved for it. It's time to start drawing the window, but before we start we must put our segment registers back where they belong. push es ; Realign Code and pop ds ; Data segments If our caller has requested it, we must ZOOM the window onto the screen. This means drawing not just one window, but a whole raft of them, each a little larger than the last, until we have one of the size required. In assembly-language, the whole process is so fast that it seems to occur in one continuous movement. It is so fast, in fact, that we will have to deliberately slow it down, so that our audience can appreciate it. The routine which does all this deserves more than just a bit-part in this article so I have given it a wider audience by making it a self- contained procedure in its' own right, one which can be called direct from QuickBASIC. You can find the source code for EXPLODE in the new version of DISPLAY.ASM which is provided with this issue. For now we just need to be aware that it is designed to use QuickBASIC's calling conventions, so we had better emulate them and pass our arguments on the stack. xor ah,ah ; Pass all parameters in AX mov al,TlRow ; Get upper-left row inc al ; Must use BASIC numbering push ax ; Pass the argument mov al,TlCol ; Get upper-left column inc al ; Must use BASIC numbering push ax ; Pass the argument mov al,BrRow ; Get lower-right row inc al ; Must use BASIC numbering push ax ; Pass the argument mov al,BrCol ; Get lower-right column inc al ; Must use BASIC numbering push ax ; Pass the argument The QBNews Page 17 Volume 2, Number 4 December 22, 1991 push [bp+12] ; Pass display attribute push [bp+6] ; Pass speed value call Explode ; Zoom the window Notice that we are calling EXPLODE, even if our caller specified no ZOOM for this window. This is not a mistake since no ZOOM is the same as a delay count of zero and just means that the window panel will appear instantaneously. We're not finished yet, though. We've still got to draw the border. That is, if one is required. mov ax,Ulc ; Get row/column co-ordinate call ScreenAddress ; Convert to memory address cmp byte ptr [bp+10],0 ; Border required? ja Pop_19 ; Yes, draw it jmp Pop_23 ; Else check for shadow The border type was the sixth parameter supplied by the calling program. We'll load it into BX so that it can be used as an index to the list of graphics characters stored in our local data. While we're at it, we'll load the display attribute required into AH Pop_19: xor bx,bx ; Border type mov bl,[bp+10] ; to BX mov ah,[bp+12] ; Attribute to AH push di ; Save screen offset We've already worked out the address of the top-left corner of the window, no need to calculate it again. Now to get the top-left corner character. TopLeft label byte db ' ÚÉÕÖÑËÂÒ' ; TL Corner characters Border types --> 012345678 <-- Argument in BX TOPLEFT is a label which refers to the string of extended ASCII top- left characters in our program data. Since BX has been set to the number of the border type required, we can use it as an index into the character string. The following instruction, therefore, loads AL with the byte stored at the address pointed to by TOPLEFT + BX. mov al,TopLeft[bx] ; Border character to AL call ScreenWrite ; Send it to the screen Once the character is in AL we can send it, and the attribute byte in AH, to the screen address pointed to by ES:DI. Our old friend, the SCREENWRITE procedure performs this for us. We now have to put the horizontal border characters along the top row of the window. Since SCREENWRITE updates the screen pointer after it writes a character, DI is already pointing to the correct address for The QBNews Page 18 Volume 2, Number 4 December 22, 1991 us to continue. mov al,Horizontal[bx] ; Border character to AL xor cx,cx ; Window width mov cl,[bp+14] ; to CX dec cl ; Subtract dec cl ; corners Load the appropriate ASCII value into AL, and the number of characters to write into CX, we can do this with a loop. Pop_20: call ScreenWrite ; Send it to the screen loop Pop_20 Finally, load the Right-hand corner character and send that out as well. That's the top row done. mov al,TopRight[bx] ; Border character to AL call ScreenWrite ; Send it to the screen Can we do the horizontal borders as a loop? 'course we can! pop di ; Recover offset add di,Increment ; Bump to next row mov al,Vertical[bx] ; Border character to AL mov cl,[bp+16] ; Window height to CX dec cl ; Subtract top and dec cl ; bottom rows Fortunately, we had the foresight to store our original starting position on the screen before SCREENWRITE incremented it. All we need do is retrieve it and add our increment value to point DI to the start of the next row. Let's get the correct vertical bar character into AL and then set CX to control the number of rows we are going to loop through. We've already done the top row and the bottom will be different again, so we can subtract two from the total number of rows. Pop_21: push cx ; Save counter push di ; Save screen pointer call ScreenWrite ; Left border mov cl,[bp+14] ; Get window width dec cl ; Point dec cl ; to shl cx,1 ; rightmost add di,cx ; column call ScreenWrite ; Right border pop di ; Recover screen pointer add di,Increment ; Bump to next row pop cx ; Recover row count loop Pop_21 ; For each row The QBNews Page 19 Volume 2, Number 4 December 22, 1991 There, that's done. Notice how we used CX again, in the middle of the loop, to calculate the offset of the character at the right border. Finally, apart from the corner characters, the bottom row is a repeat of the top .... mov al,BotLeft[bx] ; Border character to AL call ScreenWrite ; Send it to the screen mov al,Horizontal[bx] ; Border character to AL mov cl,[bp+14] ; Get window width dec cl ; Subtract dec cl ; corners Pop_22: call ScreenWrite ; Send it to the screen loop Pop_22 mov al,BotRight[bx] ; Border character to AL call ScreenWrite ; Send it to the screen ... and that's the window AND border done. Is that the lot? Pop_23: cmp byte ptr [bp+8],0 ; Shadow required? ja Pop_24 ; Yes, handle it jmp Pop_37 ; Else wrap everything up Not quite, we've still got to deal with SHADOW. Pop_24: mov di,WinTop ; Back to top-left corner add di,Increment ; start at next row down xor cx,cx ; Get window width mov cl,[bp+14] ; into CX shl cx,1 ; Include attribute bytes cmp byte ptr [bp+8],2 ; Solid shadow? ja Pop_30 ; No, make it transparant cmp byte ptr [bp+8],1 ; Left shadow? jne Pop_25 ; No, must be right dec di ; Left one dec di ; column jmp short Pop_26 ; Get to work To give the right three-dimensional effect, the shadow will have to begin one row down from the top of the window. Is it going to be a LEFT or a RIGHT shadow? If LEFT, we point DI one column to the Left of the window, if RIGHT, then DI is set to point one column past the Right-hand border. We must also distinguish between transparent and solid shadow. The first changes the display attribute of the shadowed text so that it is still visible, albeit dimly, while the second type blanks out underlying characters completely. Pop_25: add di,cx ; Offset past right-hand edge Pop_26: mov ax,720h ; Space on black background push cx ; Save width for now The QBNews Page 20 Volume 2, Number 4 December 22, 1991 mov cl,[bp+16] ; Rows to shadow The display attribute we will use for solid shadow is 7. Normal white text on a black background. The actual character is ASCII 32(20 Hex), a blank space. The shadow will be as tall as the window itself, but, since we start one row down, it will extend past the bottom. Here's the loop that does it. Pop_27: call ScreenWrite ; Send it to the screen add di,Increment ; Bump to dec di ; next dec di ; row loop Pop_27 ; For height of window pop cx ; Recover window width sub di,Increment ; Back up one row cmp byte ptr [bp+8],1 ; Left shadow? je Pop_28 ; No, must be right sub di,cx ; Jump back to start of row inc di ; Begin one column inc di ; in from left Pop_28: shr cx,1 ; Convert width to columns Pop_29: call ScreenWrite ; Put black shadow loop Pop_29 ; under the bottom row jmp short Pop_37 ; Branch to the exit If we're doing TRANSPARENT shadow, then we don't change the character byte at all. Instead we skip to the attribute byte and change that to a value of eight which produces dark grey text on a black background. WRITEBYTE is a variant of SCREENWRITE which outputs a single byte to the display instead of a character and attribute (2 bytes). Once more it is listed for you in the new version of DISPLAY.ASM. Pop_30: cmp byte ptr [bp+8],3 ; Left shadow? jne Pop_31 ; No, must be right dec di ; Left one dec di ; column jmp short Pop_32 ; Get to work Pop_31: add di,cx ; Offset past right-hand edge Pop_32: inc di ; Bump to attribute byte mov al,8 ; Dark grey foreground push cx ; Save width for now mov cl,[bp+16] ; Rows to shadow Pop_33: call WriteByte ; Set display attribute add di,Increment ; Bump to dec di ; next row loop Pop_33 ; For height of window pop cx ; Recover window width The QBNews Page 21 Volume 2, Number 4 December 22, 1991 sub di,Increment ; Back up inc di ; one row cmp byte ptr [bp+8],3 ; Left shadow? jne Pop_34 ; No, must be right dec di ; Back up one column jmp short Pop_35 ; Start on bottom row Pop_34: sub di,cx ; Jump back to the inc di ; beginning of the row Pop_35: shr cx,1 ; Convert width to columns Pop_36: call WriteByte ; Set display attribute inc di ; Bump past character byte loop Pop_36 ; For width of window And there it is. One window up on the screen, just as our calling program requested. All that remains is to tidy up and go home. Pop_37: xor ax,ax ; Report no error Pop_38: pop si ; Clean up the stack pop di pop es pop ds pop bp ret 16 ; Return to caller PopUp endp There you are. A superfast, full feature window generator, worthy to stand alongside all those professional programs on your shelf. Bill Gates, move over! Wait a minute though, it's all very well being able to pop up windows of all shapes and sizes. We also need to take them down again, when they're finished with. And what about all that screen data stored in the buffer? We've still got to put it back again. That's the business of the second routine in this module. Luckily, SHUTUP is not nearly as big as POPUP. All it has to do, in fact, is find the last block of screen data that was stored in the buffer, and restore it to it's original location on the screen, wiping out the window which covers it in the process. To make it a little fancier, however, I have added a reverse ZOOM option. This restores the screen data stored in the buffer selectively, working from the outside edge of the window inwards to the centre. This makes the window appear to implode and, depending on the speed parameter supplied, gives a very slick effect. ShutUp proc far push bp ; Save base pointer mov bp,sp ; Establish stack frame push ds ; Save segment The QBNews Page 22 Volume 2, Number 4 December 22, 1991 push es ; registers and push di ; index push si ; pointers push cs ; Align code and pop ds ; data segments cld ; Clear direction forward call VideoType ; Get video parameters mov al,ah ; Transfer number of xor ah,ah ; columns to AX shl ax,1 ; and convert mov Increment,ax ; to bytes SHUTUP is going to access the same data that was used by POPUP and, since this is at the top of our local code, we must begin by pointing the DS:SI registers to it. mov si,offset Buffer ; DS:SI==> screen buffer xor ax,ax ; Initialise push ax ; back pointer By default, all offset addresses are relative to the DS register. Now we have it positioned correctly, we can set SI to point to the start of the save buffer. What follows is very similar to the routine, in POPUP, which searched for the next free block. Remember that the first two bytes of each block is a pointer to the next one in the chain. Just as before, we must follow the pointers along until we come to an empty block. Shut_01: cmp word ptr [si],0 ; Is anything there? jz Shut_02 ; No, we're at the end pop ax ; Retrieve pointer push si ; Save present pointer mov si,[si] ; Point to next block jmp Shut_01 ; Keep searching If the first word in the buffer is zero, of course, the buffer is empty and there are no windows to restore, (it was a waste of time calling us!). Otherwise we end up pointing to the the first free block. Hang on a minute, though, it's the PREVIOUS block we want. The one which contains the last window stored ... Oh, I get it! That's why we kept saving the next-block-pointer. All we have to do is pop the last one back into SI, and we're pointing to the previous block again. Shut_02: pop si ; Retrieve last pointer cmp si,0 ; Was there anything? jnz Shut_03 ; Yes, proceed mov ax,1 ; Else set Errorlevel jmp Shut_14 ; and abort The QBNews Page 23 Volume 2, Number 4 December 22, 1991 Right, we've found the block to be restored. Now we need to extract the screen location where it's going to be restored to and the height and width of the original rectangle. If you remember they were stored along with the original data. Notice we store the window dimensions twice, once for reference and once for use as a running total as we gradually restore larger portions of the screen below the window. Shut_03: mov BuffPtr,si ; Save buffer pointer inc si ; Bump to inc si ; next entry mov di,[si] ; ES:DI==> screen location mov WinTop,di ; Save screen offset inc si ; Bump to inc si ; next entry mov ax,[si] ; Get panel dimensions mov Area,ax ; Store them mov ToDo,ax ; for later inc si ; Bump to screen inc si ; storage block mov BuffTop,si ; Save buffer pointer The next step is to calculate the addresses in the save buffer and on the screen, where the LAST row of the window begins. Using this and the address of the first row, which SI is now pointing to, as our starting points, we can work inwards on successive passes through the restore loop. xor bx,bx ; AX has window width xchg bl,ah ; BX has window height dec bl ; less one row shl bx,1 ; Convert to bytes mul bx ; Calculate offset add ax,si ; of the last row mov BuffEnd,ax ; Save this as well mov ax,Increment ; Multiply screen width mov bl,Height ; by window height dec bl ; less one row mul bx ; Result is relative offset add ax,di ; Now convert it to the mov WinEnd,ax ; absolute screen offset Before beginning, we still need to point ES to the segment address of video memory and, as ever, SCREENADDRESS proves its' worth. We had better check that the delay count supplied is legal as well, just in case the user has passed a negative argument or something silly like that. Since we are treating the argument as an unsigned value a delay of -1 would be interpreted as 65535 which, even at assembler speeds, would take a L-O-N-G time. xor ax,ax ; Get video segment call ScreenAddress ; and CRT status port cmp word ptr [bp+6],0 ; Check for The QBNews Page 24 Volume 2, Number 4 December 22, 1991 jge Shut_04 ; legal delay mov word ptr [bp+6],20 ; values Here is the start of the outer loop. We begin by loading our source and destination index registers, SI and DI, with the addresses of the top row of the window, SI being its' location in the save buffer and DI it's location on the screen. CX, which, as always, is the loop counter, gets the number of columns in this row. Shut_04: xor cx,cx ; Clear counter mov si,BuffTop ; DS:SI==> first buffer row mov di,WinTop ; ES:DI==> first screen row mov cl,Cols2do ; Number of words to copy We have already met SCREENCOPY above, when we used it to copy words from the display into our save buffer. Now we are using it in the opposite direction to move the data from the buffer back onto the screen, repeating the process until each column of this row has been restored. Shut_05: call ScreenCopy ; Send word to the screen loop Shut_05 ; For length of row dec Rows2do ; All rows done? jnz Shut_06 ; No, carry on jmp Shut_13 ; Otherwise depart With this row restored we can decrement the count of rows to be done. Eventually, when the count reduces to zero and sets the zero flag in the flags register, we can take it as a sign that the window has gone and branch out of the loop. For now, however, we must carry on and restore the bottom row, using very similar code to the top row routine, except that our index registers are pointed to the bottom row of the window, both in the buffer and on the screen. Shut_06: mov si,BuffEnd ; DS:SI==> last buffer row mov di,WinEnd ; ES:DI==> last screen row mov cl,Cols2do ; Number of words to copy Shut_07: call ScreenCopy ; Send word to the screen loop Shut_07 ; For length of row dec Rows2do ; All rows done? jnz Shut_08 ; No, carry on jmp Shut_13 ; Otherwise depart A few sums to do now. We must take the our pointer to the start of the save buffer, BUFFTOP, and add to it the number of bytes per row of the window we are restoring. This will bump the buffer pointer to the next row to be restored at the top of the window. Subtracting the same number of bytes from BUFFEND, the last row pointer, will give us The QBNews Page 25 Volume 2, Number 4 December 22, 1991 the buffer location of the next row to be restored at the bottom of the window. Shut_08: xor ax,ax ; Clear AX mov si,BuffTop ; Reset mov al,Breadth ; pointer shl al,1 ; to first add si,ax ; buffer mov BuffTop,si ; row mov di,WinTop ; Do the same add di,Increment ; for the first mov WinTop,di ; screen row A similar calculation must be made to obtain the screen addresses of the new top and bottom rows. However this is a little easier, since all we need do is add or subtract the row INCREMENT from the current pointer values. mov ax,BuffEnd ; Reset mov cl,Breadth ; pointer shl cl,1 ; to last sub ax,cx ; buffer mov BuffEnd,ax ; row mov ax,WinEnd ; Do the same sub ax,Increment ; for the last mov WinEnd,ax ; screen row We've removed the top and bottom rows, now it's time to do the left and right columns. After our previous calculations, SI and DI are now pointing to the top-left corner of what's left of the window, so we can start by removing the column on the left-hand side. Shut_09: cmp si,BuffEnd ; End of buffer? ja Shut_10 ; Yes, see if we've finished call ScreenCopy ; Send word to the screen xor ax,ax ; Clear AX again mov al,Breadth ; Keep dec al ; doing shl al,1 ; it add si,ax ; all add di,Increment ; down dec di ; the dec di ; left jmp short Shut_09 ; side Shut_10: dec Cols2do ; All columns done? jz Shut_13 ; If so, depart mov al,Cols2do ; How far to the shl al,1 ; end of the row? mov si,BuffTop ; Point SI to add si,ax ; buffer data mov di,WinTop ; Point DI to The QBNews Page 26 Volume 2, Number 4 December 22, 1991 add di,ax ; screen offset This done, we decrement the number of columns there are to do and, as long as the remainder is not zero, add the result converted to bytes, to our index registers. This points us to the top-right corner of the window, which is now shrinking fast. Away we go down the right-hand side...... Shut_11: call ScreenCopy ; Send word to the screen cmp si,BuffEnd ; End of buffer? ja Shut_12 ; Yes, see if we're finished xor ax,ax ; Clear AX again mov al,Breadth ; Keep dec al ; doing shl al,1 ; it add si,ax ; all add di,Increment ; down dec di ; the dec di ; right jmp short Shut_11 ; side Shut_12: dec Cols2do ; All columns done? jz Shut_13 ; If so, depart By the time we reach here we have done a complete circuit of our window, lopping off a row or column on all four sides. If there is still more to do, we must adjust the pointers to the screen and buffer to take the reduced number of columns into account. add BuffTop,2 ; Each subsequent add BuffEnd,2 ; row starts add WinTop,2 ; another word add WinEnd,2 ; further in Although it has taken a long time to describe, the actual processing has been done in less than the twinkle of an eye. So if we want our audience to actually see how clever we are, we must slow things down to human speeds. It's a good job computers don't get bored.... push [bp+6] ; Pass speed value call Delay ; Pause awhile jmp Shut_04 ; Then do it all again Well, to us it didn't take long. The window is gone and the screen is back in its' original pristine state. Only one thing left to do. We must also clear this block from our save buffer. With our usual foresight, we saved the pointer to the start of the block. Notice that we're retrieving it into DI, this time, not SI. There's a good reason for this which I'll explain in a minute. In the meantime, we need to calculate the length of the block, since we're going to set every byte to zero. The QBNews Page 27 Volume 2, Number 4 December 22, 1991 Shut_13: mov di,BuffPtr ; Recover buffer pointer mov cx,[di] ; Pointer to next block sub cx,di ; Calculate length of block inc cx Amongst the many instructions built into the Intel 8086 series of microprocessors, is a set of very powerful string manipulation routines. The one we're going to use is STOSB (STOre Byte to String). This stores the contents of the AL register into the address pointed to by the ES:DI registers, and then increments DI. Not much in itself, but if you prefix STOSB with the REP instruction, the process is REPeated the number of times set by CX. This way we can fill a large block of memory with a single line of code, instead of having to set up an elaborate loop. ES:DI is already pointing to the start of the block. CX contains the length of the block, in bytes. All we have to do is load AL with the value to fill, in this case zero. push ds ; Point ES to Data Segment pop es xor ax,ax ; Clear AX rep stosb ; Zero restored block Shut_14: pop si ; Clean up the stack pop di pop es pop ds pop bp ret 2 ; Return to caller ShutUp endp end That's it, we've done it. Our window manager is complete. Have you still got the strength to add it to the library? ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ GETTING IT ALL TOGETHER ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ Two object files are provided. WINDOWER.OBJ contains the two window procedures, POPUP and SHUTUP, described above. DISPLAY.OBJ contains FASTPRINT and all the support routines used in my previous article, along with EXPLODE and the other new routines added in this issue. If you are using the BASIC 7 PDS with far strings, then, instead of DISPLAY.OBJ you must use the object file DISPLAY7.OBJ which is also provided. WINDOWER will work equally well with either version of the compiler. To produce a new version of your Assembly-Language Library, copy the The QBNews Page 28 Volume 2, Number 4 December 22, 1991 object files along with your existing copy of ASSEMBLY.LIB into the directory which contains the QuickBASIC Library Manager LIB.EXE. Then issue the following command: LIB ASSEMBLY +WINDOWER -+DISPLAY,ASSEMBLY.CAT; This will add WINDOWER.OBJ to ASSEMBLY.LIB and replace the existing version of DISPLAY.OBJ with the new one. The command also tells LIB.EXE to generate an updated version of your library catalogue file ASSEMBLY.CAT. Producing the matching Quick Library, ASSEMBLY.QLB, is just as easy. Using the new copy of ASSEMBLY.LIB you have just produced, type: LINK /QU ASSEMBLY.LIB,,,BQLB45.LIB; Notice that LINK.EXE can work just as easily with complete libraries as it does with individual object files. The Quick Library support file BQLB45.LIB, (QBXQLB.LIB if you use BASIC 7), must also be either present, or on your environment search path. To use the window routines in your programs you must include the following declarations at the beginning of the source code: DECLARE SUB PopUp(BYVAL Row%, BYVAL Col%, BYVAL Hght%, BYVAL Wdth%,_ BYVAL Attr%, BYVAL Brdr%, BYVAL Shdw%, BYVAL Zoom%) DECLARE SUB ShutUp(BYVAL Speed%) Then, whenever you want to pop up a window, issue a statement like this ... PopUp 4, 10, 8, 50, 48, 2, 1, -1 .. which produces a window with its top-left corner at row 4, column 10. This window is eight rows high by fifty columns wide, it has a black, double-lined border on a cyan background (if you have a colour monitor), with shadow on the left-hand side and, when it appears, it will Z-O-O-O-M onto the screen. You can pass variables or expressions to POPUP as well as constants, but they must all evaluate to integers, otherwise the routine may refuse to pop! To get rid of the window, and restore the screen contents, use the statement ... ShutUp -1 Remember that SHUTUP always removes the most recent window. EXPLODE, the external procedure which is called by POPUP to zoom the window onto the screen, can also be called directly from QuickBASIC. If you want to use it outside of the window routines, then you must declare it seperately with the following statement: DECLARE SUB Explode (BYVAL Y1%, BYVAL X1%, BYVAL Y2%, BYVAL X2%,_ The QBNews Page 29 Volume 2, Number 4 December 22, 1991 BYVAL Attr%, BYVAL Speed%) Arguments: Y1% = Upper-left row of rectangle to be cleared X1% = Upper-left column of rectangle Y2% = Lower-right row of rectangle X2% = Lower-right column of rectangle Attr% = Display attribute or colour that rectangle should be cleared to Speed% = Speed (in milliseconds) of explosion. The example program, WINDEM.BAS, uses EXPLODE whenever it needs to clear text from a window on the screen, without having to remove the window itself. It can also be used as a general-purpose routine for whenever you need to selectively clear a part of the screen, without effecting the rest of the display. You can disable the exploding part by specifying a delay of zero. ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ If you have a problem getting these routines to work with your system, or you have any comments or suggestions for future programs, I would like to hear from you. Christy Gemmell 22 Peake Road, Northfields Leicester LE4 7DN England Tel. (044)-0533-767960 If transatlantic mail is too slow (or the telephone charge too high), I can also be reached via: Jim Kreyling (sysop) Club-PC BBS 1217 Crescent Drive, Smithfield, Va.23430 Tel. (804)-357-0357 BBS Tel. (804)-357-9190 FAX or Voice Club-PC are also US distributors of my Assembly-Language Toolbox for QuickBASIC which contains a full set of window and display routines, as well as lots of other useful features. Shareware versions are available both for QuickBASIC 4.5 and BASIC 7 and can be downloaded free from this board. ********************************************************************** Christy Gemmell resides in England and was the major author of the Waite Group book QuickBASIC Bible. His new book is The Waite Group's QBASIC Bible. Christy also has a shareware called the Assembly Language Toolbox for QuickBASIC. Christy can be reached in care of this newsletter. **********************************************************************