Home |
More Timers MultiMedia and Thread timers |
Home |
In the BrushPens Program, a Timer was used and the WM_TIMER message was sent to the MessageProc and this caused the Splash Window to change it's text. Using SetTimer( ) is a convient way to get timed events if you want them for intervals of more than about 200 miliseconds. For intervals below 200 milliseconds, the accuracy of the System Timer is very poor. So we will use some alteratives, a Multimedia timer and a Thread timer. This Timers program demonstrates how to use SetTimer( ) with a separate timer message function to do the same type of Splash Screen. Another type of timer, a Multi-Media timer is shown and is used to do animations. The Multi-Media Timer is much more accurate than the System Timer that times the SetTimer( ) function. Another type of timer, a Thread Timer will be created to show you how to create a separate thread and let a SleepEx( ) function fire a timer event. To show the difference in the accuracy of the System and Mutimedia timers, two identical Text Scrolling functions are used, one fired by the System Timer and the other fired by the Mutimedia Timer, so you can see the results in how fast the text is scrolled. In order to measure the real time interval of these timers, I will use the Performance Counter, which can get more than a million counts per second, and can be used to time even the fastest events. A common use for the Mutimedia timer is for graphic animations and sprite movement. I will show how to do a moving ball animation, where a small picture of a ball is drawn at sequential locations of a rectangle. Two different methods of animation are shown here, one just paints the ball on the display and the other paints the entire rectangle to avoid flicker.
We have used the SetTimer( ) function for a system timer before so we will talk about using the TimerProc function with that next.
Using the TimerProc with SetTimer( ) Here we use a TimerProc fuction instead of the WM_TIMER message to fire the timer event. Look at the TimerProc in the index of the Win32 API Help. When you call the SetTimer( ) function you will need to give the address of your TimerProc in the last parameter like this - SetTimer(hForm1,4,1000, @TimerProc);Otherwise, SetTimer( ) is used the same as was demonstrated in the BrushPen program with the WM_TIMER message in the WindowProc function. If you give a method address in the last parameter of SetTimer( ) then the WM_TIMER message will call thet procedure instead of doing the message. If you look at the TimerProc in the Timers Program code below you will see TimerProc(Wnd: HWnd; Mesg, TimerID, SysTime: Longint); stdcall;The 4 parameters are like the 4 parameters in MessageProc( ) and the Wnd as hForm1 and Mesg as WM_TIMER. We will test the TimerID parameter to get the ID number and execute the code for that TimerID. The SysTime parameter is the system TicCount which the GetTickCount( ) function returns. the MultiMedia Timer The Multimedia timer allows developers to schedule timer events with the greatest resolution (or accuracy) possible for that computer's hardware. These multimedia timers allow you to schedule timer events at a higher resolution than other timer services, especialy the System timer using the SetTimer( ) function. These Multimedia timers are useful for applications that demand high-resolution timing. For example, a MIDI sequencer requires an accurate timer because it must maintain the sequence of MIDI events within a resolution of 1 millisecond. Unlike the WM_TIMER message used for the SetTimer( ) function, the Mutimedia Timer can triger it's callback function while you program is gong other processing. To get the minimum and maximum timer resolutions supported by MutiMedia timers, use the timeGetDevCaps( ) function. This function fills the wPeriodMin and wPeriodMax members of the TTimeCaps record with the minimum and maximum resolutions allowed. This range can vary across computers and Windows platforms or versions. After you get the minimum and maximum available MultiMedia timer resolutions, you must set the minimum resolution you want your application to use. Use the timeBeginPeriod( ) and timeEndPeriod( ) functions to set and clear this resolution. You must match each function call to timeBeginPeriod( ) with a function call to timeEndPeriod( ), giving the same minimum resolution in both calls. A program can make multiple timeBeginPeriod( ) calls, as long as each of these is matched with a call to timeEndPeriod( ). After you have set your mutimedia timer resolution, you can start timer events by using the timeSetEvent( ) function. This function returns a timer identifier that can be used to stop or identify timer events. The third parameter in this function is the address of a TimeProc callback function that is called when a timer event takes place. There are two types of multimedia timer events: single and periodic. A single timer event occurs only once, after a specified number of milliseconds. A periodic timer event keeps occuring every time a specified number of milliseconds elapses until it is stopped. The interval between periodic events is called an event delay. Periodic timer events with an event delay of 10 milliseconds or less consume a significant portion of CPU resources. The relationship between the resolution of a timer event and the length of the event delay is important in timer events. For example, if you set a resolution of 5 and an event delay of 100, the multimedia timer can notify the callback function after an interval ranging from 95 to 105 milliseconds. You can cancel an active timer event at any time by using the timeKillEvent( ) function. Be sure to cancel any outstanding timers before ending the program or thread that has the TimerProc function. You must include the mmsystem unit in your Uses clause to use the mutimedia timer functions. the MultiMedia Timer is in a separate Thread In order to maintain high timer resolutions, the multimedia timer runs in its own Windows System thread. This OS thread is NOT suspended by a System-Modal dialog box, so if you press the famous Ctrl - Alt - Delete keys and get the Close Program Dialog box, your program's thread is suspended, but the Mutimedia Timer thread keeps on going and calling the TimerProc, so when the System-Modal is closed all the mutimedia timer events that were called during the dialog box's existance will be called without delay when the dialog box closes. the Thread Timer Instead of using a Mutimedia timer you can create a separate thread and have that thread sleep with the SleepEX( ) function and fire an event each time the SleepEx( ) returns. A thread is the basic unit to which the operating system allocates processor time. A thread can execute any part of the process code, including parts currently being executed by another thread. Windows OS supports preemptive multitasking, which creates the effect of simultaneous execution of multiple threads. Multitasking divides the available processor time among the processes or threads that need it. Creating separate threads is used in API to allow code execution for one thread not to be heald up by the code execution in another thread. I will not attempt to try and explain all the dymanics and factors of separate threads. You can read some of the aspects of it in the Win32 API Help under the index of "Processes and Threads". If you look at the code in the DoThreadTimer( ) procedure in the Timer Program below you will see hThread := BeginThread(nil, 0, @ThreadFunc, nil, 0, ThreadId);The BeginThread( ) function starts a sparate thread. Since the third parameter is @ThreadFunc the function ThreadFunc(Parameter: Pointer): Integer; stdcall;will be where this thread will executed. This ThreadFunc( ) is much like the .dpr program code execution, as in a .dpr, the threads execution starts at "begin" and ends at "end;" So you will need a loop to keep this thread alive, if you were creating windows in this thread you would use a Message loop, but we are not creating any windows and will use a While loop with a SleepEx(TimerR.Interval, False); in it. The While loop runs as long as the thdTiming is true, so you need to set thdTiming to true before you start this thread. See the comments in the code below for more info. |
This program creates a Splash screen like the Brushs and Pens program but it draws directly on the desktop HDC instead of creating a Splash screen Form to draw on. It uses SetTimer( ) to do the Splash screen timing for the letter display and changing to the main Form. Here we use a procedure named "TimerProc" to process the WM_TIMER events. See the code in the TimerProc for more info. This Program creates 4 buttons and a Check Box on a form about 580x420 pixels in size. You should notice that in the Win Class setup the style is set to CS_OWNDC Style := CS_BYTEALIGNCLIENT or CS_OWNDC; The CS_OWNDC gives this window a "Persistant" long Lasting HDC. Called a Private Device Context, a unique device context for a window in that class. So you need only get the HDC once and then use it for all subsequent painting. Although the CS_OWNDC style is convenient, use it carefully, because each device context uses a portion of system resources. Since we are doing graphic animation here the Private HDC is helpful since we do not need to call GetDC( ) in the thousands of calls for painting the moving ball. Two Text Scrolling Boxes are Drawn in the WM_PAINT message using the DrawEdge(PaintS.hDC, ScrolRect1, EDGE_SUNKEN, BF_RECT); function. This gives the appearence of a windowed control (an Edit Box). If you click the "Start Scroll" button the "procedure ScrollText;" is called to create a background Bitmap for the Scrolling Text Boxs. Then the TimeSetEvent( ) and the SetTimer( ) are called to start the 2 timers with the same interval of 38 milliseconds. The SetTimer( ) time event will fire the TimerProc to draw a new rectangle in the upper Text Scroll box and the TimeSetEvent( ) time event will fire the MMTimerProc to draw the scrolling text in the lower box. see comments in the code below for more info |
program Timers; {this program uses 3 different ways to do timed events. Here the WM_TIMER message is not used for the SetTimer function, but a special TimerProc fuction is used. This also demonstrates the use of a Multi-Media Timer, which is more accurate than the SetTimer. And an example of a Timer event fired in a separate Thread using SleepEx( )} uses Windows, Messages, SmallUtils, mmsystem; {mmsystem is needed for a Multi Media Timer} {$R *.RES} type ThreadRec = record Interval: Cardinal; ID: Word; end; var wClass: TWndClass; hForm1, hExitBut, hScrollBut, hSpriteBut, hPFlipBut, hThreadCB: THandle; Font1, Brush1, Brush2, Brush3, Pen1, hThread: THandle; SpriteBmp1, MaskBmp, FlipBmp, BkgndBmp: THandle; mainMsg: TMSG; Rect1, ScrolRect1, ScrolRect2: TRect; FormDC, Bmp1DC, MaskDC, FlipDC, BkgndDC: HDC; FontLog1: TLogFont; DrawTimes: Byte; Posit1, Posit2: Integer; BrushLog1: TLogBrush; Scrolling, GetPer, GetPer1, Moving, GoRight, GoDown, Fliping, thdTiming: Boolean; Size1: TSize; TimeCaps1: TTimeCaps; TimerID1, TickCount: Cardinal; PerFreq, PerCount0, PerCount1, PerCount2, PerCount3: Int64; BallPoint: TPoint; TimerR: ThreadRec; const Splash = 'Timers'; TextStr1 = 'This is a WM_TIMER Scroll'; TextStr2 = 'This is a MM Timer Scroll'; procedure ShutDown; begin KillTimer(hForm1,1); KillTimer(hForm1,2); KillTimer(hForm1,3); KillTimer(hForm1,4); {make sure timers are dead} thdTiming := False; timeKillEvent(TimerID1); TimeEndPeriod(TimeCaps1.wPeriodMin); {be sure to timeKillEvent and TimeEndPeriod before you close your app} CloseHandle(hThread); {release the OS references to the thread handle} DeleteObject(Font1); DeleteObject(Brush1); DeleteObject(Brush2); DeleteObject(Brush3); DeleteObject(Pen1); ReleaseDC(hForm1, FormDC); DeleteDC(Bmp1DC); DeleteDC(MaskDC); DeleteDC(FlipDC); DeleteDC(BkgndDC); DeleteObject(SpriteBmp1); DeleteObject(MaskBmp); DeleteObject(FlipBmp); DeleteObject(BkgndBmp); PostQuitMessage(0); end; function MakeBrush(Color1, Color2: Cardinal): THandle; var BmpDC: hDC; i, k, Dot: Integer; Bitmap1: THandle; TempDC: HDC; begin {this creates a diagonal stripe bitmap brush with 2 colors} TempDC := GetDC(0); Bitmap1 := CreateCompatibleBitmap(TempDC,8,8); BmpDC := CreateCompatibleDC(TempDC); ReleaseDC(GetDesktopWindow,TempDC); SelectObject(BmpDC,Bitmap1); Dot := 0; for i:= 0 to 7 do begin for k:= 0 to 7 do if ((k+Dot) mod 4) <> 0 then SetPixelV(BmpDC,i,k, Color1) else SetPixelV(BmpDC,i,k, Color2); Inc(Dot); end; BrushLog1.lbStyle := BS_PATTERN; BrushLog1.lbHatch := Bitmap1; Result := CreateBrushIndirect(BrushLog1); DeleteDC(BmpDC); DeleteObject(Bitmap1); end; function MakeBrush3: THandle; var BmpDC: hDC; Bitmap2: THandle; i, k, Dot: Integer; TempDC: HDC; begin {this makes a bitmap brush with alternating black and white pixels} TempDC := GetDC(0); {this function is called before the Form window is created, so we get the desktop hDC} Bitmap2 := CreateBitmap(8,8,1,1,nil); BmpDC := CreateCompatibleDC(TempDC); ReleaseDC(GetDesktopWindow,TempDC); SelectObject(BmpDC,Bitmap2); Dot := 0; for i:= 0 to 7 do begin for k:= 0 to 7 do if ((k+Dot) mod 2) <> 0 then SetPixelV(BmpDC,i,k,$00000000) else SetPixelV(BmpDC,i,k,$00FFFFFF); Inc(Dot); end; BrushLog1.lbStyle := BS_PATTERN; BrushLog1.lbHatch := Bitmap2; Result := CreateBrushIndirect(BrushLog1); DeleteDC(BmpDC); DeleteObject(Bitmap2); end; procedure TimerProc(Wnd: HWnd; Mesg, TimerID, SysTime: Longint); stdcall; var TimeRec: TSystemTime; Time: String; OldColor: Cardinal; OldMode: Integer; OldFont: THandle; begin {the WM_TIMER message fires this TimerProc function, if the address of this function is given in SetTimer( )} case TimerID of 1 : begin {this stops the Splash Screen and shows the main Form} KillTimer(hForm1,1); KillTimer(hForm1,2); ReleaseDC(GetDesktopWindow,MaskDC); MaskDC := 0; ShowWindow(hForm1, SW_SHOWNORMAL); SetTimer(hForm1,4,1000, @TimerProc); end; //1 2 : begin {this is the Splash Screen Painting, unlike the BrushPen program this does not create splash window, but just draws on the desktop. DrawTimes is used to know which splash letter to draw} if DrawTimes = 0 then PatBlt(MaskDC, Rect1.Left+80, Rect1.Top+30, 300, 250,PATCOPY) else if DrawTimes < 7 then TextOut(MaskDC,0,0,@Splash[DrawTimes],1) else PatBlt(MaskDC, Rect1.Left+80, Rect1.Top+30, 300, 250,DSTINVERT); Inc(DrawTimes); if DrawTimes > 8 then KillTimer(hForm1,2); end; //2 3 : begin {this is the Scroll Text painting} {FormDC := GetDC(hForm1); SelectObject(FormDC, Brush2); SetBkMode(FormDC,TRANSPARENT); the 3 functions above are NOT needed because we are using a Private Device Context FormDC, for our main Form} if GetPer1 and (PerFreq <> 0) then begin {the PerformanceCounter is a very fast high-resolution counter which can be used to find out the amount of time between 2 calls to QueryPerformanceCounter( )} QueryPerformanceCounter(PerCount3); GetPer1 := False; SetBkMode(FormDC,OPAQUE); Time := Int2Str(((PerCount3-PerCount2)*10000) div PerFreq); Insert('.',Time,Length(Time)); TextOut(FormDC,6,ScrolRect1.Top+6,PChar(Time), Length(Time)); {this will show the milliseconds between the PerCount2 and PerCount3 Query which should be equal to the Interval in SetTimer, but the interval will be about 55 no matter what you set it to below 55, 55 millisecond is about as fast as the system timer can go} SetBkMode(FormDC,TRANSPARENT); Time := ''; end; if Posit1 > Size1.cx+17 then begin {Posit1 will be larger than Size1.cx+17 only once per scroll cycle} Posit1 := 0; if PerFreq <> 0 then begin QueryPerformanceCounter(PerCount2); GetPer1 := True; end; end; InflateRect(ScrolRect1,-2,-2); PatBlt(FormDC, ScrolRect1.Left, ScrolRect1.Top, 196, 26,PATCOPY); ExtTextOut(FormDC,ScrolRect1.Left-Posit1, ScrolRect1.Top+4, ETO_CLIPPED, @ScrolRect1, TextStr1, 25, nil); {ExtTextOut( ) clips the text output to the ScrolRect1} ExtTextOut(FormDC,ScrolRect1.Right-Posit1, ScrolRect1.Top+4, ETO_CLIPPED, @ScrolRect1, TextStr1, 25, nil); Inc(Posit1); InflateRect(ScrolRect1,2,2); end; //3 4: begin {this timer event fires about once a second and draws the local Date and Time on the form, like a clock} GetLocalTime(TimeRec); {to get date and time you can use GetLocalTime} OldFont := SelectObject(FormDC, GetStockObject(ANSI_VAR_FONT)); OldColor := SetBkColor(FormDC, GetSysColor(COLOR_BTNFACE)); OldMode := SetBkMode(FormDC, OPAQUE); {since a Private DC is used for this form, all DC Objects and modes set here will remain for the FormDC even after this function, so you need to record the DC settings you change and reset them} Time := 'Local Date - '+Int2Str(TimeRec.wMonth)+'/'+Int2Str(TimeRec.wDay)+ '/'+Int2Str(TimeRec.wYear)+' Time - '+Int2Str(TimeRec.wHour)+':'+ Int2Str(TimeRec.wMinute)+' '+Int2Str(TimeRec.wSecond)+' Seconds '; TextOut(FormDC,128,365,@Time[1],Length(Time)); {after the text is drawn reset the FormDC settings back to what they were, or other animation drawing operations will be affected} SelectObject(FormDC, OldFont); SetBkColor(FormDC, OldColor); SetBkMode(FormDC, OldMode); Time := ''; end; end; // case end; procedure MoveBall; var PCstr: String; begin {this animates a sprite (a picture of a ball and shadow), so it moves around a Rectangle and appears to bounce off the edges} if GetPer and (PerFreq <> 0) then begin {this will display the actual time elasped for this timer notice that it is the interval set in the Muti-Media or Thread timer} QueryPerformanceCounter(PerCount1); GetPer := False; SetBkMode(FormDC,OPAQUE); PCstr := Int2Str(((PerCount1-PerCount0)*100000) div PerFreq); Insert('.',PCstr,Length(PCstr)-1); TextOut(FormDC,460,120,PChar(PCstr), Length(PCstr)); SetBkMode(FormDC,TRANSPARENT); end; SelectObject(FormDC, Brush1); {SetROP2(FormDC,R2_COPYPEN);} PatBlt(FormDC, BallPoint.x, BallPoint.y, 49, 44,PATCOPY); {BallPoint is used to remember the position of the ball GoRight and GoDown are used to set the direction of movement} if BallPoint.x > 366 then begin GoRight := False; if PerFreq <> 0 then begin QueryPerformanceCounter(PerCount0); GetPer := True; end; end else if BallPoint.x < 104 then GoRight := True; {the ball changes direction when the position gets to the max 366 and min 104 values, which corresponds to the rectangle's position minus the width of the sprite bitmap} if GoRight then Inc(BallPoint.x,5) else Dec(BallPoint.x,4); {changing the BallPoint X and Y will move the ball. You need to match the amount of pixel change with the timer interval to get the speed of ball movement you want} if BallPoint.y > 213 then GoDown := False else if BallPoint.y < 12 then begin GoDown := True; if PerFreq <> 0 then begin QueryPerformanceCounter(PerCount0); GetPer := True; end; end; if GoDown then Inc(BallPoint.y,3) else Dec(BallPoint.y,2); {television uses 25 frames per second (an inteval of about 40 milliseconds). You might try to keep your animation interval between 30 and 40 milliseconds and adjust your pixel Increase for movement, to get the ball movement speed you want} SelectObject(FormDC, Brush3); {SetRop2 is R2_MASKPEN and will draw the Elipse so only the black pixels of Brush3 will be drawn, a shadow effect} Ellipse(FormDC,BallPoint.x+4,BallPoint.y+5,BallPoint.x+49,BallPoint.y+44); {2 paints are required to get a transparent bitmap, first the MaskBmp is drawn with the ScrAnd raster operation. Next the Inverted colors Bmp1 is drawn with the SrcInvert, so the colors will go back to normal and the transparent mask area will go back to the background hDC colors} BitBlt(FormDC, BallPoint.x, BallPoint.y, 40, 40, MaskDC, 0, 0, SrcAnd); BitBlt(FormDC, BallPoint.x, BallPoint.y, 40, 40, Bmp1DC, 0, 0, SrcInvert); end; procedure FlipIt; var RectP: TRect; {Counts: Cardinal;} begin {if you watch the MoveBall animation above you will see the ball flicker sometimes, if the background paint and the ball paint are in a different screen refresh, then you get flicker - - In this FlipIt animation, a method called Page-Flipping is used. Instead of drawing the new background and ball on the screen, a bitmap is used to do this painting off screen, and then the whole bitmap is drawn onscreen, and then there is no refresh between the background and ball drawing, and no flickering. However, bitmaps can get so large that doing animation of many sprites or Scanlines and other graphics operations on them, and then drawing the bitmap on screen takes longer than the animation timer interval, so you can have limits for page flipping} if PerFreq <> 0 then QueryPerformanceCounter(PerCount2); BitBlt(FlipDC, BallPoint.x, BallPoint.y, 49, 47, BkgndDC, BallPoint.x, BallPoint.y, SrcCopy); {Erase the sprite by copying the BkgndBmp over it} if BallPoint.x > 240 then GoRight := False else if BallPoint.x < 4 then GoRight := True; if GoRight then Inc(BallPoint.x,5) else Dec(BallPoint.x,4); if BallPoint.y > 196 then GoDown := False else if BallPoint.y < 2 then GoDown := True; if GoDown then Inc(BallPoint.y,3) else Dec(BallPoint.y,2); {now draw a new sprite on the FlipBmp} Ellipse(FlipDC,BallPoint.x+4,BallPoint.y+5,BallPoint.x+49,BallPoint.y+44); BitBlt(FlipDC, BallPoint.x, BallPoint.y, 40, 40, MaskDC, 0, 0, SrcAnd); BitBlt(FlipDC, BallPoint.x, BallPoint.y, 40, 40, Bmp1DC, 0, 0, SrcInvert); BitBlt(FormDC, 100, 10, 380, 250, FlipDC, 0, 0, SrcCopy); {the entire FlipBmp is now copied to the FormDC instead of a smaller section to cover the sprite. Since there is only ONE draw operation to the Screen (FormDC) there is no flicker} if PerFreq <> 0 then begin {this will show the High Performance ticks required to draw on the bitmap and dwaw the bitmap on the form} QueryPerformanceCounter(PerCount3); {Counts := ((PerCount3-PerCount2)*1000) div PerFreq;} {to get the time in milliseconds you need to divide by the PerFreq but this bitmap is small enough that it takes less than 1 millisecond} SetBkMode(FormDC,OPAQUE); SetRect(RectP,424,5,474,35); FillRect(FormDC,RectP, wClass.hbrBackground); TextOut(FormDC,424,5,PChar(Int2Str({Counts}PerCount3-PerCount2)), Length(Int2Str({Counts}PerCount3-PerCount2))); SetBkMode(FormDC,TRANSPARENT); end; end; procedure ScrollIt; var Ticks: String; begin {This animates a text display so it looks like the text is scrolling from left to right. It is the same as the scroll text in the TimerProc number 3: So you can see the difference between a Multi-Media Timer and the system Timer WM_TIMER animation.} {FormDC := GetDC(hForm1); SelectObject(FormDC, Brush2); SetBkMode(FormDC,TRANSPARENT);} {the functions above do NOT need to be called because we are using a Private DC} if GetPer and (PerFreq <> 0) then begin {this will display the actual time elasped for this timer notice that it is the interval set in the Muti-Media or Thread timer} QueryPerformanceCounter(PerCount1); GetPer := False; SetBkMode(FormDC,OPAQUE); TextOut(FormDC,ScrolRect2.Right+8,ScrolRect2.Top+6,PChar('Ticks '+ Int2Str(GetTickCount-TickCount)), Length('Ticks '+Int2Str(GetTickCount-TickCount))); Ticks := Int2Str(((PerCount1-PerCount0)*10000) div PerFreq); Insert('.',Ticks,Length(Ticks)); TextOut(FormDC,6,ScrolRect2.Top+6, PChar(Ticks{Int2Str(((PerCount1-PerCount0)*1000) div PerFreq)}), Length(Ticks{Int2Str(((PerCount1-PerCount0)* 1000) div PerFreq)})); SetBkMode(FormDC,TRANSPARENT); Ticks := ''; end; if Posit2 > Size1.cx+17 then begin Posit2 := 0; if PerFreq <> 0 then begin QueryPerformanceCounter(PerCount0); TickCount := GetTickCount; GetPer := True; end; end; {the Size1 is for different text, TextStr1, than is used here, but it gives the same scroll width as the scrolling in TimerProc for comparision} InflateRect(ScrolRect2,-2,-2); BitBlt(FormDC, ScrolRect2.Left, ScrolRect2.Top, Size1.cx+17, ScrolRect2.Bottom- ScrolRect2.Top, BkgndDC, Posit2, 0, SrcCopy); BitBlt(FormDC, ScrolRect2.Right-Posit2, ScrolRect2.Top, Posit2, ScrolRect2.Bottom- ScrolRect2.Top, BkgndDC, 0, 0, SrcCopy); {PatBlt(FormDC, ScrolRect2.Left, ScrolRect2.Top, 196, 26,PATCOPY); ExtTextOut(FormDC,ScrolRect2.Left-Posit2, ScrolRect2.Top+4, ETO_CLIPPED, @ScrolRect2, TextStr2, 25, nil); ExtTextOut(FormDC,ScrolRect2.Right-Posit2, ScrolRect2.Top+4, ETO_CLIPPED, @ScrolRect2, TextStr2, 25, nil);} InflateRect(ScrolRect2,2,2); {ReleaseDC(hForm1, FormDC);} Inc(Posit2,2); end; procedure MMTimerProc(uTimerID, uMessage: cardinal; dwUser, dw1, dw2: integer); stdcall; begin {you should keep all operations in other procedures and not do anything here except call those procedures. This Fuction will be called by the Muti-Media timer even if the procedures called from a previous MMTimerProc have NOT yet completed. The Multi-Media timer runs in a separate thread.} case dwUser of 1: MoveBall; 2: FlipIt; 10: ScrollIt; else timeKillEvent(TimerID1); end; end; function ThreadFunc(Parameter: Pointer): Integer; stdcall; begin {you should keep all operations in other procedures and not do anything here except call those procedures. This Fuction runs in a separate thread and will be called even if the procedures from a previous ThreadFunc have NOT yet completed.} Result := 0; while thdTiming do begin {thdTiming cuts this thread off, it uses a while loop with a SleepEx to get a timer interval effect} SleepEx(TimerR.Interval, False); case TimerR.ID of 1: if thdTiming then FlipIt; {check for thdTiming here also in case it changed during SleepEx} 2: if thdTiming then MoveBall; else Break; end; end; TimerR.ID := 0; EndThread(Result); {ExitThread(Result);} {no code below EndThread will be executed} TextOut(FormDC,4, 100, 'Below EndThread', 15); end; procedure DoThreadTimer(Interval, TimerID: Word); var ThreadId: LongWord; begin {The interval should be more than 4 or alot of the processor time cycles are used, if you need an interval less than 5 then you may want to set the Thread Priority higher with SetThreadPriority( )} if TimerR.ID <> 0 then Exit; {TimerR.ID is set to 0 when the Thread ends, so check for 0 to make sure you don't start a thread if one is still running} if Interval < 5 then TimerR.Interval := 5 else TimerR.Interval := Interval; TimerR.ID := TimerID; CloseHandle(hThread); {since a thread may be created more than once, you should Close the Handle} {if you wanted to use more than one thread then you should keep track of each thread's handle and close it} hThread := BeginThread(nil, 0, @ThreadFunc, nil, 0, ThreadId); {BeginThread( ) is a Delphi System function which does the same as windows CreateThread( ), except it sets the global IsMultiThread variable, thereby making the heap thread-safe. Below is the CreateThread( ) function, which will also work} {hThread := CreateThread( NIL, // no security attributes 0, // use default stack size @ThreadFunc, // thread function nil, // argument to thread function 0, // use default creation flags ThreadId);} {SetThreadPriority(hThread, THREAD_PRIORITY_ABOVE_NORMAL);} {use EndThread( ) with BeginThread( ) and use ExitThread( ) with CreateThread( ), see ThreadFunc( ) above} end; procedure ScrollText; var Interval,Accur: Word; begin if Scrolling then begin Scrolling := False; timeKillEvent(TimerID1); KillTimer(hForm1,3); EnableWindow(hSpriteBut, True); EnableWindow(hPFlipBut, True); EnableWindow(hThreadCB, True); SetWindowText(hScrollBut, 'Start Scroll'); DeleteDC(BkgndDC); DeleteObject(BkgndBmp); PatBlt(FormDC, ScrolRect1.Left+2, ScrolRect1.Top+2, 196, 26,PATCOPY); PatBlt(FormDC, ScrolRect2.Left+2, ScrolRect2.Top+2, 196, 26,PATCOPY); TextOut(FormDC,ScrolRect1.Left+4, ScrolRect1.Top+6, TextStr1, 25); TextOut(FormDC,ScrolRect2.Left+4, ScrolRect2.Top+6, TextStr2, 25); Posit1 := 1; Posit2 := 1; end else begin EnableWindow(hSpriteBut, False); EnableWindow(hPFlipBut, False); EnableWindow(hThreadCB, False); SelectObject(FormDC, Brush2); SetBkMode(FormDC,TRANSPARENT); InflateRect(ScrolRect2,-2,-2); BkgndBmp := CreateCompatibleBitmap(FormDC, Size1.cx+17, ScrolRect2.Bottom- ScrolRect2.Top); BkgndDC := CreateCompatibleDC(FormDC); SelectObject(BkgndDC, BkgndBmp); SelectObject(BkgndDC, Brush2); SetBkMode(BkgndDC, TRANSPARENT); PatBlt(BkgndDC, 0, 0, Size1.cx+17, ScrolRect2.Bottom- ScrolRect2.Top, PATCOPY); TextOut(BkgndDC,3, 4, TextStr2, 25); InflateRect(ScrolRect2,2,2); timeKillEvent(TimerID1); Interval := 38; Accur := Interval div 2; if Accur < TimeCaps1.wPeriodMin then Accur := TimeCaps1.wPeriodMin; if Accur > TimeCaps1.wPeriodMax then Accur := TimeCaps1.wPeriodMax; if TimeCaps1.wPeriodMin <> 0 then TimerID1 := TimeSetEvent(Interval, Accur, @MMTimerProc, 10, TIME_PERIODIC); SetTimer(hForm1,3,Interval, @TimerProc); {notice that the Interval for both the MM timer and the system timer are the same} Scrolling := True; SetWindowText(hScrollBut, 'Stop Scroll'); TextOut(FormDC,ScrolRect1.Left,ScrolRect1.Top-28,PChar('Scroll Interval is '+ Int2Str(Interval)), 19+Length(Int2Str(Interval))); end; end; procedure MakeMoveBall; var TDC: HDC; TempBmp, TempBrush, TempPen: THandle; begin {this creates the animation sprite bitmap, it adds a highlight and some shadow arcs to a circle, so it will have some 3D and look like a ball} Bmp1DC := CreateCompatibleDC(FormDC); MaskDC := CreateCompatibleDC(FormDC); TDC := CreateCompatibleDC(FormDC); SpriteBmp1 := CreateCompatibleBitmap(FormDC,40,40); MaskBmp := CreateCompatibleBitmap(FormDC,40,40); {since this is suppose to look like a ball and bitmaps are retangular, we need to draw a Transparent bitmap. To do this we need a mask bitmap with the area we want to be transparent as white, and the area to be seen as black} TempBmp := CreateCompatibleBitmap(FormDC,40,40); {the TempBmp will get an Inverted color sprite and then the Inverted bitmap will be copied to the sprite bitmap} SelectObject(Bmp1DC, SpriteBmp1); SelectObject(MaskDC, MaskBmp); SelectObject(TDC, TempBmp); SelectObject(MaskDC, GetStockObject(WHITE_BRUSH)); PatBlt(MaskDC, 0, 0, 40, 40,PATCOPY); SelectObject(Bmp1DC, GetStockObject(WHITE_BRUSH)); PatBlt(Bmp1DC, 0, 0, 40, 40,PATCOPY); SelectObject(MaskDC, GetStockObject(BLACK_BRUSH)); Ellipse(MaskDC,0,0,40,40); TempBrush := CreateSolidBrush($00AADD00); SelectObject(Bmp1DC, TempBrush); Ellipse(Bmp1DC,0,0,40,40); SelectObject(Bmp1DC, GetStockObject(WHITE_BRUSH)); {the white brush is used to add a highlight} PatBlt(Bmp1DC, 10, 8, 5, 5,PATCOPY); TempPen := CreatePen(PS_SOLID, 1, $00669900); SelectObject(Bmp1DC, TempPen); {several Arcs are drawn with a darker pen to add a shadow effect to the ball} Arc(Bmp1DC,2,2,38,38,18,40,40,18); DeleteObject(TempPen); TempPen := CreatePen(PS_SOLID, 1, $0099AA00); SelectObject(Bmp1DC, TempPen); Arc(Bmp1DC,5,5,35,35,20,40,40,20); DeleteObject(TempPen); TempPen := CreatePen(PS_SOLID, 1, $0099BB00); SelectObject(Bmp1DC, TempPen); Arc(Bmp1DC,8,8,32,32,22,40,40,22); DeleteObject(TempPen); TempPen := CreatePen(PS_SOLID, 1, $00D2E4B4); SelectObject(Bmp1DC, TempPen); Arc(Bmp1DC,3,3,37,37,16,0,0,16); BitBlt(TDC, 0, 0, 40, 40, MaskDC, 0, 0, SrcCopy); BitBlt(TDC, 0, 0, 40, 40, Bmp1DC, 0, 0, SrcErase); {to get a transparent bitmap effect we copy the MaskDC onto the TDC, then we do an Erase operation with the Bmp1DC, which will invert the Bmp1DC bitmap} BitBlt(Bmp1DC, 0, 0, 40, 40, TDC, 0, 0, SrcCopy); {the temp TDC with the inverted color ball is copied to the Bmp1DC} DeleteDC(TDC); DeleteObject(TempBmp); DeleteObject(TempBrush); DeleteObject(TempPen); end; procedure EndAnimation; begin thdTiming := False; timeKillEvent(TimerID1); EnableWindow(hScrollBut, True); EnableWindow(hPFlipBut, True); EnableWindow(hThreadCB, True); EnableWindow(hSpriteBut, True); DeleteDC(Bmp1DC); DeleteDC(MaskDC); DeleteObject(SpriteBmp1); DeleteObject(MaskBmp); BallPoint.x := 150; BallPoint.y := 100; end; procedure Animate; var Interval,Accur: Word; begin if Moving then begin Moving := False; EndAnimation; SetWindowText(hSpriteBut, 'Start Move'); {SelectObject(TempDC, GetSysColorBrush(COLOR_BTNFACE)); PatBlt(TempDC, 100, 10, 320, 250,PATCOPY);} {ReleaseDC(hForm1, TempDC);} end else begin timeKillEvent(TimerID1); EnableWindow(hScrollBut, False); EnableWindow(hPFlipBut, False); EnableWindow(hThreadCB, False); SetWindowText(hSpriteBut, 'Stop Move'); Moving := True; QueryPerformanceCounter(PerCount0); {TempDC := GetDC(hForm1);} MakeMoveBall{(TempDC)}; SelectObject(FormDC, Brush1); PatBlt(FormDC, 100, 10, 320, 250,PATCOPY); {SetBkMode(TempDC, TRANSPARENT);} BitBlt(FormDC, BallPoint.x, BallPoint.y, 40, 40, MaskDC, 0, 0, SrcAnd); BitBlt(FormDC, BallPoint.x, BallPoint.y, 40, 40, Bmp1DC, 0, 0, SrcInvert); SelectObject(FormDC, GetStockObject(NULL_PEN)); QueryPerformanceCounter(PerCount1); SetBkMode(FormDC,OPAQUE); TextOut(FormDC,35,5,PChar(Int2Str(PerCount1-PerCount0)), Length(Int2Str({(}(PerCount1-PerCount0){* 1000) div PerFreq}))); Interval := 27; {shorter Intervals do faster movement, but use up MORE processor time. You should increase the change in pixel position for faster movement and increase your Interval if your Interval gets below about 30} Accur := Interval div 2; if Accur < TimeCaps1.wPeriodMin then Accur := TimeCaps1.wPeriodMin; if Accur > TimeCaps1.wPeriodMax then Accur := TimeCaps1.wPeriodMax; if SendMessage(hThreadCB, BM_GETSTATE, 0, 0) = BST_CHECKED then begin Interval := 18; thdTiming := True; DoThreadTimer(Interval, 2); {12 milliseconds less time than you should use, you should double this time to 24 milliseconds and double your pixel change in MoveBall from Inc(BallPoint.x,5) else Dec(BallPoint.x,4); to Inc(BallPoint.x,10) else Dec(BallPoint.x,8);} end else if TimeCaps1.wPeriodMin <> 0 then TimerID1 := TimeSetEvent(Interval, Accur, @MMTimerProc, 1, TIME_PERIODIC); TextOut(FormDC,430,92,PChar('Move Interval is '+Int2Str(Interval)), 17+Length(Int2Str(Interval))); SetBkMode(FormDC,TRANSPARENT); {SetRop2 is set to R2_MASKPEN and will draw the Elipse in MoveBall so only the black pixels of Brush3 will be drawn, giving a shadow effect} SetROP2(FormDC,R2_MASKPEN); end; end; procedure PageFlip; var TempBrush: THandle; Interval,Accur: Word; begin if Fliping then begin Fliping := False; EndAnimation; SetWindowText(hPFlipBut, 'PageFlip move'); DeleteDC(FlipDC); DeleteDC(BkgndDC); DeleteObject(FlipBmp); DeleteObject(BkgndBmp); end else begin EnableWindow(hScrollBut, False); EnableWindow(hSpriteBut, False); EnableWindow(hThreadCB, False); Fliping := True; SetWindowText(hPFlipBut, 'Stop Flip'); SelectObject(FormDC, GetSysColorBrush(COLOR_BTNFACE)); PatBlt(FormDC, 100, 10, 320, 250,PATCOPY); MakeMoveBall; BkgndBmp := CreateCompatibleBitmap(FormDC,280,240); BkgndDC := CreateCompatibleDC(FormDC); SelectObject(BkgndDC, BkgndBmp); SelectObject(BkgndDC, Brush2); PatBlt(BkgndDC, 0, 0, 280, 240,PATCOPY); SelectObject(BkgndDC, Font1); TempBrush := CreateSolidBrush($00FF0000); SelectObject(BkgndDC, TempBrush); Ellipse(BkgndDC,10,30,270,210); DeleteObject(TempBrush); SetBkMode(BkgndDC, TRANSPARENT); TextOut(BkgndDC,17,70,'Page Flip', 9); FlipBmp := CreateCompatibleBitmap(FormDC,280,240); FlipDC := CreateCompatibleDC(FormDC); SelectObject(FlipDC, FlipBmp); BitBlt(FlipDC, 0, 0, 280, 240, BkgndDC, 0, 0, SrcCopy); SelectObject(FlipDC, Brush3); SelectObject(FlipDC, GetStockObject(NULL_PEN)); SetROP2(FlipDC,R2_MASKPEN); QueryPerformanceCounter(PerCount1); SetBkMode(FormDC,OPAQUE); TextOut(FormDC,35,5,PChar(Int2Str(PerCount1-PerCount0)), Length(Int2Str(((PerCount1-PerCount0)* 1000) div PerFreq))); Interval := 24; Accur := Interval div 2; if Accur < TimeCaps1.wPeriodMin then Accur := TimeCaps1.wPeriodMin; if Accur > TimeCaps1.wPeriodMax then Accur := TimeCaps1.wPeriodMax; if SendMessage(hThreadCB, BM_GETSTATE, 0, 0) = BST_CHECKED then begin Interval := 34; thdTiming := True; DoThreadTimer(Interval, 1); end else if TimeCaps1.wPeriodMin <> 0 then TimerID1 := TimeSetEvent(Interval, Accur, @MMTimerProc, 2, TIME_PERIODIC); TextOut(FormDC,430,70,PChar('Flip Interval is '+Int2Str(Interval)), 17+Length(Int2Str(Interval))); SetBkMode(FormDC,TRANSPARENT); end; end; function MessageProc(hWnd: HWnd; Msg: UINT; WParam: WPARAM; LParam: LPARAM): UINT; stdcall; var PaintS: TPaintStruct; begin case Msg of WM_PAINT: begin BeginPaint(hWnd, PaintS); DrawEdge(PaintS.hDC, ScrolRect1, EDGE_SUNKEN, BF_RECT); {the Scrolling text boxes need a sunken edge drawn to make them look like a windows control} DrawEdge(PaintS.hDC, ScrolRect2, EDGE_SUNKEN, BF_RECT); TextOut(PaintS.hDC,6,202,PChar('PerFreq='+Int2Str(PerFreq)), 8+Length(Int2Str(PerFreq))); if not Scrolling then begin SelectObject(PaintS.hDC, Brush2); SetBkMode(PaintS.hDC,TRANSPARENT); PatBlt(PaintS.hDC, ScrolRect1.Left+2, ScrolRect1.Top+2, 196, 26,PATCOPY); PatBlt(PaintS.hDC, ScrolRect2.Left+2, ScrolRect2.Top+2, 196, 26,PATCOPY); TextOut(PaintS.hDC,ScrolRect1.Left+4, ScrolRect1.Top+6, TextStr1, 25); TextOut(PaintS.hDC,ScrolRect2.Left+4, ScrolRect2.Top+6, TextStr2, 25); end; if Moving then begin SelectObject(PaintS.hDC, Brush1); PatBlt(PaintS.hDC, 100, 10, 320, 250,PATCOPY); end; EndPaint(hWnd,PaintS); end; WM_COMMAND: if lParam = abs(hExitBut) then PostMessage(hForm1, WM_CLOSE,0,0) else if lParam = abs(hScrollBut) then ScrollText else if lParam = abs(hSpriteBut) then Animate else if lParam = abs(hPFlipBut) then PageFlip; WM_DESTROY: ShutDown; end; Result := DefWindowProc(hWnd,Msg,wParam,lParam); end; begin // / / / / / / / / main BEGIN / / / DrawTimes := 0; Scrolling := False; GetPer := False; GetPer1 := False; Moving := False; Fliping := False; GoRight := True; GoDown := True; thdTiming := False; BallPoint.x := 150; BallPoint.y := 100; Posit1 := 1; Posit2 := 1; TimerID1 := 0; TimerR.ID := 0; hThread := 0; SetRect(ScrolRect1,40,276,240,306); SetRect(ScrolRect2,40,316,240,346); {ScrolRect1 and ScrolRect2 are used to paint the text scrolling boxes} Brush1 := MakeBrush($003366FF, $00FFFFFF); Brush2 := MakeBrush($00EEF0FF,$0033E0EF); Brush3 := MakeBrush3; Pen1 := CreatePen(PS_SOLID, 4, $00990099); with FontLog1 do begin lfHeight := -60; lfWidth := 0; lfItalic := 0; lfWeight := FW_NORMAL; lfCharSet := ANSI_CHARSET; lfOutPrecision := OUT_TT_PRECIS; lfClipPrecision := CLIP_DEFAULT_PRECIS; lfQuality := ANTIALIASED_QUALITY; lfPitchAndFamily := VARIABLE_PITCH or FF_SWISS; lfFaceName := 'Comic Sans MS'; end; Font1 := CreateFontIndirect(FontLog1); wClass.hInstance := hInstance; with wClass do begin Style := CS_BYTEALIGNCLIENT or CS_OWNDC; {the style is set to CS_OWNDC, this will make the Form1 window have a Private Device Context hDC. I use this style because animations use rapid drawing, with a private hDC you do not have to GetDC and ReleaseDC with each animation sprite painting} hIcon := LoadIcon(hInstance,'MAINICON'); lpfnWndProc := @MessageProc; hbrBackground := COLOR_BTNFACE+1; lpszClassName := 'Form Class'; hCursor := LoadCursor(0,IDC_ARROW); end; RegisterClass(wClass); SetRect(Rect1,0,0,574,394); if not AdjustWindowRect(Rect1,WS_CAPTION or WS_MINIMIZEBOX or WS_SYSMENU,False) then SetRect(Rect1,0,0,580,420); hForm1 := CreateWindow(wClass.lpszClassName, ' Timers', WS_CAPTION or WS_MINIMIZEBOX or WS_SYSMENU , (GetSystemMetrics(SM_CXSCREEN) div 2)-276, (GetSystemMetrics(SM_CYSCREEN) div 2)-222, Rect1.Right-Rect1.Left, Rect1.Bottom-Rect1.Top, 0, 0, hInstance, nil); FormDC := GetDC(hForm1); {the default hDC font will be used for drawing the scrolling text} GetTextExtentPoint32(FormDC, TextStr1, 25, Size1); {since CS_OWNDC is in the Window Class style, using ReleaseDC(hForm1, FormDC); has no effect here} hExitBut :=CreateWindow('Button','E x i t', WS_VISIBLE or WS_CHILD or BS_PUSHBUTTON or BS_TEXT, 484,360,74,24,hForm1,0,hInstance,nil); hScrollBut := CreateWindow('Button','Start Scroll', WS_VISIBLE or WS_CHILD or BS_PUSHBUTTON or BS_TEXT, 20,360,78,24, hForm1,0, hInstance,nil); SendMessage(hScrollBut,WM_SETFONT,GetStockObject(ANSI_VAR_FONT),0); hSpriteBut := CreateWindow('Button','Start move', WS_VISIBLE or WS_CHILD or BS_PUSHBUTTON or BS_TEXT, 310,290,78,24, hForm1,0, hInstance,nil); SendMessage(hSpriteBut,WM_SETFONT,GetStockObject(ANSI_VAR_FONT),0); hPFlipBut := CreateWindow('Button','PageFlip move', WS_VISIBLE or WS_CHILD or BS_PUSHBUTTON or BS_TEXT, 420,290,86,24, hForm1,0, hInstance,nil); SendMessage(hPFlipBut,WM_SETFONT,GetStockObject(ANSI_VAR_FONT),0); hThreadCB := CreateWindow('Button', 'Use Thread Timer', WS_CHILD or BS_AUTOCHECKBOX or WS_VISIBLE, 350,328,110,24,hForm1,0,hInstance,nil); SendMessage(hThreadCB,WM_SETFONT,GetStockObject(ANSI_VAR_FONT),0); if TimeGetDevCaps(@TimeCaps1, sizeof(TimeCaps1)) = TIMERR_NOERROR then TimeBeginPeriod(TimeCaps1.wPeriodMin) else TimeCaps1.wPeriodMin := 0; {the 2 lines TimeGetDevCaps and TimeBeginPeriod above are NEEDED to set up your MultiMedia Timer which is much better for movement, animation, sprite drawing than a SetTimer.} if not QueryPerformanceFrequency(PerFreq) then PerFreq := 0; {QueryPerformanceFrequency gets the number of times a second this PerformanceCounter ticks, it can be above a million ticks per second} GetWindowRect(hForm1, Rect1); MaskDC := GetDC(0); {this program draws it's Splash Screen on the desktop, it is better NOT to draw on the desktop, I do it here to show that you can draw on the desktop, but you should create a splash window to draw on, like the BrushPen program} SelectObject(MaskDC, Font1); SetTextColor(MaskDC, $003366FF); SetBkColor(MaskDC,$00DDEE33); SetTextCharacterExtra(MaskDC, 8); SetTextAlign(MaskDC, TA_UPDATECP); MoveToEx(MaskDC,Rect1.Left+106,Rect1.Top+108, nil); SelectObject(MaskDC,Pen1); SelectObject(MaskDC, GetStockObject(NULL_BRUSH)); Rectangle(MaskDC, Rect1.Left+78, Rect1.Top+28, Rect1.Left+383, Rect1.Top+283); SelectObject(MaskDC,Brush1); {2 timers are started, timer "2" will progressivly draw some text for the splash screen, Timer "1" will end the splash screen and show the main Form} SetTimer(hForm1,2,330, @TimerProc); SetTimer(hForm1,1,4500, @TimerProc); while GetMessage(mainMsg,0,0,0) do begin TranslateMessage(mainMsg); DispatchMessage(mainMsg); end; end. |