0

I'm using Delphi 7 and the program I am writing needs to continuously draw on the screen. While it currently doesn't draw anything important, this is a necessity in the program later on. However, when I put the procedure for drawing the screen in a while loop which can only be stopped by pressing any button the program stops responding completely. I don't understand why this is happening. Surely, as the while loop can be exited, the program should continue to run fine. Here is the source code:

unit DD04f1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, TeCanvas, ExtCtrls;

type
  TForm1 = class(TForm)
    Image1: TImage;
    Button1: TButton;
    procedure Image1OnCreate();
    procedure ScreenRender();
    procedure OnCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure FormKeyDown(Sender: TObject; var Key: Word;
      Shift: TShiftState);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;
  IsDone : Boolean;

implementation

{$R *.dfm}
procedure TForm1.OnCreate(Sender: TObject);
begin
  IsDone := False;
end;

procedure TForm1.Image1OnCreate ();
var
  Count:Integer;
begin
  image1.canvas.Create();
  image1.canvas.Pen.Color:=clBlack;
  image1.canvas.rectangle(0,0,640,480);
  image1.canvas.Pen.Color:=$ed630e; //bgr instead of rgb

  Count:=0;
  While (Count <> 640) do
  begin
    image1.Canvas.moveto(Count,0);
    image1.Canvas.LineTo(Count,480);
    Count:=Count+1;

  end;
end;

procedure TForm1.ScreenRender();
var
  Count : Integer;
begin
  Count:=0;
  While(Count<>640) do
  begin
    image1.Canvas.moveto(Count,0);
    image1.Canvas.LineTo(Count,480);
    Count:=Count+1;
  end;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
    Image1OnCreate();
    Button1.Visible := False;
    While(IsDone = False) do
    begin
      ScreenRender();
    end;
end;

procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  IsDone := True;
end;

end.
DoYouEvenFish
  • 35
  • 1
  • 6
  • You're not allowing processing of Windows messages. Your code is flawed. Use the debugger to step through and figure out why. As written, it will not respond to anything while the loop is running, including attempts to move the form, resize it, or anything else UI related. I'm not even sure how it's getting the message to stop the loop the way it's written now. – Ken White Nov 09 '17 at 23:56
  • How would I allow processing of Windows messages? (Sorry if this seems obvious) – DoYouEvenFish Nov 10 '17 at 00:02
  • Well, the proper way is not to create a loop like that in the first place, that uses 100% CPU time for absolutely no reason. The minimum (and ill-advised) solution is to insert a call to `Application.ProcessMessages` inside your loop, but that's the absolute worst solution. I'd figure out why you have to have such poor code in the first place that needs 100% CPU. I think you've created an XY problem here; you're looking for a kludge to fix a problem you wouldn't have in the first place if you were doing things correctly. I can't tell you how to solve for Y. You haven't asked about it. – Ken White Nov 10 '17 at 00:15
  • 4
    If it were me, I'd delete this post, rethink what you're trying to do that makes you think you need this loop, and then post a new question about how to do that thing without the loop. You've not explained anything here about why you think it's a *necessity to constantly draw to the screen*. I *can* say that any time the solution to something is Application.ProcessMessages, you're doing something wrong. – Ken White Nov 10 '17 at 00:18
  • 1
    In any case though, if you need to "constantly" draw an image, you should be using the `OnPaint` event, or `WM_PAINT` message, and paint only when Windows tells you it's ready for you to paint. But since you're painting to a `TImage` instead of a control canvas, I really don't know what you want to do. – Jerry Dodge Nov 10 '17 at 00:19
  • @Jerry: Damn, you beat me to it with WM_PAINT (although I was going to write OnPaint instead). That was in my next comment. – Ken White Nov 10 '17 at 00:20
  • @JerryDodge the program is going to be an untextured raycasting, so each vertical line of the screen needs to be drawn on. The code above is the framework for it so that I can change how much of that vertical line is drawn and what colour. I'm assuming that, as what is drawn to the screen changes, the OnPaint event wouldn't be what is best. But I'm not sure, I'm very new to Delphi so I don't know which would be better to use. – DoYouEvenFish Nov 10 '17 at 00:25
  • It depends on what the result of your painting will be used for. Will it be for some sort of animation in your app (since you mention "what is drawn to the screen changes")? If an animation is what you're trying to do, then doing so in a loop as you are now is definitely not where you need to go. – Jerry Dodge Nov 10 '17 at 00:27
  • @JerryDodge it's not for an animation. Instead it's for calculating and then drawing what the player should be able to see based off of their x,y location and their orientation. Also, if you know of any, could either of you two point me towards any resources about Delphi, as the only ones I have don't mention anything about Windows Messages etc. – DoYouEvenFish Nov 10 '17 at 00:31
  • 1
    You're probably better off learning about the plain Windows API itself first, as Delphi's VCL framework is highly based on it - including and especially the Windows Message system. Long story short, as long as you have any sort of loop running, Windows is unable to process the message queue. The message queue is responsible for handling user input and painting to the window. – Jerry Dodge Nov 10 '17 at 00:34
  • I'll take a look at the Windows API then, thank you for your help! – DoYouEvenFish Nov 10 '17 at 00:36
  • 1
    @Jerrry: Yeah, I got more caught up in pointing out what appeared to be an XY problem. This code is poorly designed, and the question asked is entirely wrong. The question should be asking about the proper way to do the continuous drawing in the first place instead of how to kludge around the looping problem. – Ken White Nov 10 '17 at 00:37

2 Answers2

2
procedure TForm1.OnCreate(Sender: TObject);
begin
  IsDone := False;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
    Image1OnCreate();
    Button1.Visible := False;
    While(IsDone = False) do
    begin
      ScreenRender();
    end;
end;

Assuming IsDone is always False (because otherwise we would not enter the loop), this loop can not terminate. It is infinite.

procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  IsDone := True;
end;

You do not call this procedure from inside TForm1.Button1Click loop, hence it can never be called after you entered that loop. Since you never exit the TForm1.Button1Click procedure you do not allow any outside agent (like messages dispatch loop in VCL) to get executed and call that procedure either. To sum it up as soon as you entered the loop there is no any executable code that can change IsDone value. So, it is not changed.

Event handlers are supposed to be very short procedures, executing almost immediately, and giving up "execution flow control" back to VCL internals. Every long (more so infinite) processing leads to the program becomes irresponsive. No matter how many news Windows might want to tell the program - the program never asks for them.

It was once told that Windows windows (GDI objects) are living in the center of the "messages storm" that they have to work out in timely matter. Hundreds of those messages are incoming every second and a Window Procedure (built inside the VCL classes for Delphi 7 forms) should receive, dispatch, and process every one of them before it's too late.

As soon as you blocked that process by making one of event handlers long or even endless - you broke the basic contract between the OS and the application.

You have to do "inversion of control", to break your continuous work into small short chunks and make Windows call those chunks when it sees appropriate. Try to use TTimer for example.

PS. A VERY remote problem you can look at:

Skip all the multithreading stuff there, for your case it only is important that other threads create those "chunks of work" that we have to paint onto our forms when Windows asks us to do so at some reasonable framerate (not too fast and not too slow). Your work chunks are fundamentally different, so all the threading stuff unrelated to you.

And the rendering is made inside TTimer events. So the "framework" of setting up the timer, turning it on and off might be of some interest to you. However the work you are going to do inside the .OnTimer event would be significantly different (just painting something, or even merely invalidating some part of the form and waiting for the Windows to trigger OnPaint event.).

Arioch 'The
  • 15,799
  • 35
  • 62
2

You already got an excellent answer why your current code does not work and in your comments you are mentioning you want to do ray casting and drawing from a players perspective, so I assume some kind of game background.

I'm not sure the VCL is the best basis for a game. Different philosophies and needs. As Arioch 'The explained Delphi's VCL is event driven. Things happen in response to windows messages, even painting. If nothing causes a need to repaint, nothing will be painted anew.

This is very different from how I understand game engines (I'm by no means an expert). Even if nothing happens, they will continuously draw frame after frame to present as fluid as possible. Each frame might include an update to underlying structures based on game rules, physics, player input, animation, but even when they remain the same a new frame will be drawn. Basically three steps happen in a simplified 'game loop'

  • Input
  • Update
  • Presentation

All this happens for every frame. There might be no input, no update of the game's structures or even no presentation is desired. But all three steps belong together, the input causing an update that is later presented happened in the exact same frame as the resulting drawing.

This is something I find hard to fit into the VCL. As a solution must be based on the existing VCL loop and windows messages. You basically attempted to create such a game loop in VCL.

A way to solve your immediate issue - that you want to present something based on a calculation - could be just using the principle of the VCL. You want to have something drawn. VCL controls normally communicate their desire to be drawn by Invalidate, causing their BoundsRect to be invalidated. You could do that after you have done your calculations. In the following example I'll just use a timer to simulate your calculations are done. Just be aware that Invalidate will cause WM_PAINT messages to be generated for the control, but will not cause immediate repainting. There might be messages queued before the WM_PAINT is processed. I'm using a TPaintBox's OnPaint to actually do the painting work, you might want to have your own control for that in the future when your project progresses.

unit Unit2;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ExtCtrls;

type
  TFormMain = class(TForm)

    procedure FormCreate(Sender: TObject);
  private
    Timer1: TTimer;
    PaintBox1: TPaintBox;
    { Private declarations }
    procedure PaintBox1Paint(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
  public
    { Public declarations }
  end;

implementation

{$R *.dfm}

procedure TFormMain.FormCreate(Sender: TObject);
begin
  PaintBox1 := TPaintBox.Create(Self);
  PaintBox1.Parent := Self;
  PaintBox1.Align := alClient;
  PaintBox1.OnPaint := PaintBox1Paint;

  Timer1 := TTimer.Create(Self);
  Timer1.Interval := 100;
  Timer1.OnTimer := Timer1Timer;
  Randomize;
end;

procedure TFormMain.PaintBox1Paint(Sender: TObject);
var
  AColor: TColor;
  I: Integer;
begin
  for I := 0 to PaintBox1.ClientWidth - 1 do
  begin
    AColor := RGB(Random(256), Random(256), Random(256));
    PaintBox1.Canvas.Pen.Color := AColor;
    PaintBox1.Canvas.MoveTo(I, 0);
    PaintBox1.Canvas.LineTo(I, PaintBox1.ClientHeight);
  end;
end;

procedure TFormMain.Timer1Timer(Sender: TObject);
begin
  PaintBox1.Invalidate;
end;

end.
nil
  • 1,320
  • 1
  • 10
  • 21
  • Frankly there were some libraries for making simple 2D and 3D games in Pascal, both Delphi and FreePascal/Lazarus. Most of them were abandoned after people proved it is possible and lost interest. But still they are more or less usable. If topicstarter wants to do some game design, perhaps he is to start with googling for those libs and for reading some general books on game design (like matrix-calculus for combining multiple 3D transformations into one action) – Arioch 'The Nov 10 '17 at 15:07
  • @Arioch'The what I would find interesting is if these libraries were based on VCL. For hobby purposes I experimented a bit with DirectX and C#, first thing I learned from that is that a properly designed game loop seems to be the basis. Doing that on your own lets you have full control. The VCL with it's own message loop seems to be contradicting in this regard. – nil Nov 10 '17 at 15:14
  • Frankly, Windows GDI never was designed for action games and even before Windows 95 (see 16-bits library of 1994: https://en.wikipedia.org/wiki/WinG ) – Arioch 'The Nov 10 '17 at 15:41
  • Abusing VCL/GDI (pull technology) for making an action game (push technology) is possible, but hardly pragmatic. It is either the quest for glory (yes, I can make this stuff do the work is was made to avoid) or some very niche imposed necessity. Like, there is DosBox rewritten as Flash plugin in HTML page, which is abuse of technology in itself. But it provides for playing 1990-s DOS games in modern tablets and computers :-D But what may be a practical reason to embed an action game into GDI/VCL? I see none. – Arioch 'The Nov 10 '17 at 15:55