HOME - - - - - Lazarus Tutorials TOC - - - - - - Other material for programmers
Delicious.Com Bookmark this on Delicious     StumbleUpon.Com Recommend to StumbleUpon

An advanced tutorial in Lazarus and Delphi Graphics

Using as example: Tool for stock market investors- Part 2

Sorry... this page needs a LOT of editing... for html, etc... not so much for the contents. The Lazarus code is done, working... it is just the write up that is faulty.

Many general techniques are illustrated along the way. A compiled .exe is available so that you can play with the application without having to compile it. The Lazarus sourcecode is in the same zip file you can download. Both are there for you to play with... but I reserve all rights to any applications derived from the sourcecode. It is there to show you programming techniques, not to be the basis of a commercial product. Well... anyone else's commercial product, anyway.

I hope you came to this web page after being enthralled by two earlier pages which sets out how you might create an application to draw a small chart of stock prices on your computer's screen.

The result so far of that exercise looked something like this....

-

The diagrams are supposed to indicate my guess as to the "boundaries" within which the stock "should" trade over the coming weeks. Once this program is done, I hope to create further programs, ones which will automatically "watch" the companies I've done "green line diagrams" for, and alert me when a company acts differently from my expectations.

This tutorial is very heavily rooted in the one that comes before it, LT3Nf-2.htm. We are "merely" extending the application built in that tutorial, LD009.

We've already done quite a bit of work, making a "good start". Now we are going to go on.

Where we are going

We need to...

Whew! These somewhat abstract things can be challenging. Happily, we now return to the practical.

I'm going to leave to your imagination some of the "integrating" of these new ideas and tools and subroutines into our growing app. I'll do another full listing in a moment.

What will be in that is something that has the two green lines on a graph of stock prices which we have been talking about. The "Do it" button is gone... the graph draws, including some green (and blue) lines, when appropriate. Resizing works. (Maintaining the position of the green lines between resizes was "interesting". The resizing mechanism remains crude, but it has been written in a manner which will, I hope, one day allow for simple dragging for resize. (The window can be resized by dragging, but not the graph. For that, you enter your wishes, click a "Resize" button.)

While the old "Change Data" button will remain... we will need it eventually... the application will launch with a default set of data in place. (For now, the change data will do the things it will eventually need to do... apart from actually switching to a different data set!)

The Draw Right and Draw Left buttons have gone, as they were only for testing little odds and ends.

Bad news....

This whole thing was taking many, MANY more days than I had available for it.

While my dream of one day being able to adjust the position of the green lines simply by dragging the places they cross the blue lines remains alive, it has been, for now, been put on a back burner. Instead, for now, I will have four sliders... one for each blue line/ green line intersection. And I will continue to develop the program with a view to one day converting the user interface to the drag-with-mouse alternative.

To that end, the first scrollbar has been re-named sbUpperRight. And three more have been added... sbLowerRight, sbUpperLeft, sbLowerLeft.... all with their own associated variables, initializations, etc, etc. Sigh.

Re scaling the X axis is not yet possible, but the program is written to make that easy, in due course. (Famous last words! For now, we are working with a fixed set of data. The dates available don't change... yet.

In the FormCreate handler, there is an array called bYStart, elements 0..3. Currently, the values are 40,10,60,50. These are values, expressed as percents of the height of GraphArea for the FIRST Y coordinate of the ends of the two green lines. The "default" position for a green line, in other words. They should be for where the green lines cross the blue lines, but that's not the case yet. The ends are taken in the order you would read letters on a page: upper left, upper right, lower left, lower right. The first value, 40, means that the left hand end of the upper green line will be 40% of the way UP from the bottom of the GraphArea. (Remember: The bitmap coordinates run the other way, but this is to be hidden from users.)

As soon as a user moves a green line, the values in bYStart become "old news". The current Y coordinates are held in iYPrev[0..3], with the array elements assigned the same way. These values, never seen by users, are in GraphArea coordinates, e.g. 0,0 would be the upper left of the GraphArea.

The variables wGrGAWidth and wGrGAHeight hold the current size of the GraphArea. the pixel at bitmap[wGrGAWidth,wGrGAHeight] would be just to the right of, just BELOW the current GraphArea.

The scrollbars are calibrated in GraphArea units.

"Teach" the application to read the stock date/price data from a datafile.

We're going to implement something crude first. Then we need to discuss some things that need ini files, then we are going to come back to this.

There are several approached to using datafiles. We will stipulate that we are dealing with data in files which can be looked at, even manipulated (if you are careful) with a simple text editor like Textpad. (Windows' Notepad, if you must.)

Many times you can get away with just moving the whole file to a memo, and working with the data from there.

In this case, we are using a more "grown up" approach. We will "open" the file, read some of it, and, as we "opened" it, we need eventually, to "close" it. None of this is rocket science. You do need to be careful that for each opening there is a closing.

I've also written a tutorial on basic ideas, data files, if you want that. It was written for Delphi, but applies equally well to Lazarus, I think (complain if you find problems!)

The data in our first data file is going to be very similar to the data we had in the data stream simulator we have been using to date....

<!--NOT code-->
41746-2300
41745-2280
41744-2270
41743-2265
41742-2270
41741-2245
41740-2198
41739-2196
41738-2195
41737-2190
41736-2180
41735-2155
41734-2158
41733-2165
41732-2175
41731-2180
41730-2175
41729-2180
41728-2175
41727-2180
9999-1000
0000-1000

Just to remind you: The first number is a date, in days since about 1900. The hyphen splits the record. The second number is a price for a stock (but could of course be other things, if they boil down to a number.) Records are now split by the invisible "new line" code, and the records are in order, newest at top. I'm using Textpad, and it seems to use Hex 0D, 0A, in that order, for newline.

The two strange records at the end are merely scraps of my indecision as to how "end of data" should be indicated. In the final version, no such rogue values will be needed. In the meantime, we will try to be careful to stop reading the file based on reaching a certain date, and try to be careful always to choose a date which is after the last date for which the file has data.

Where? What?

}}NEEDS EDIT}}}}}}

Before we go any further, a little "digression"... we're going to set up two variables to say WHERE the datafile we want will be found, and WHAT its name will be. (Later, we will be using "standard", "fixed" code for the reading, but it will be "informed" of where to find the data, what the file's name is, from these variables. The values in the variables won't change... for the moment. But they will, in due course. We will call them sDatePriceFilesPath and sDatePriceFilename. The value in the first should always end with a \, so that the two may simply be concatenated to obtain a full path spec.

Before we set up the variables, we'll decide on where these things will be, for the moment, anyway. By using the variables well, we will build in flexibility, allowing the users to put their data where they want it.

For now, for this, we will have our data in the folder "C:\LDN010Data\Prices". And the file of dates and prices we're using for the moment will be called ABC.txt, "ABC" being the "ticker" of an imaginary company, made up for these purposes. (You can put your data where you want... you will only have to change one thing in the code that follows.)

A detail: Within ChangeDataStep1 and DrawGraph, you will find a local variable dfDatePrice. The work of each is very similar. Why not use just one global variable? By declaring that variable twice, separately, below the global level, I am hoping that I will remember that in each procedure we open, read and close the datafile. Try to have as few global variables as possible.

{{END NEEDS EDIT{{{{

The subroutine ChangeDataStep1 reads through the data which is going to be plotted, to pick up the X and Y maxima and minima up to the first date that is too old to be of interest to us.

In subroutine DrawGraph, we read the file again (as far as we need to), this time plotting points on the graph as we proceed.

I knew where I was going when I wrote what has gone before. I wrote it with making the following easy: "Plugging in" the "read from file" was quite straightforward, mostly concerning doing the opening and closing of the data stream a little differently.

I can honestly say that, for once, that actually went according to plan. The conversion from the internal hard-coded data to accessing external text files wasn't too awful. Whew! (Of course, I did write my first "read data from textfile" app at least 30 years ago...)

We will put the values in sDatePriceFilesPath and sDatePriceFilename into the general ini file which we are just about to create, so that users have an convenient way to tailor the application's behavior to suit the users wants.

Provide the app with two ini files...

Ini file for general inits...

We'll start with a simple ini file which just "remembers" where we want the window to appear, and what size it should be. We'll assume that we are going to use the window in the "restored" state, i.e. not minimized, not maximize.

The application's main ini file will be assumed to be in the same folder as the .exe file. it will be called LDN010ini.txt, which will be held in string variable sinifileName.

I'm not going to go into the detail of that here. You can see it in the next listing of the sourcecode, and all I've used is described in my tutorial on ini files.

We had to add code to FormCreate, and event handlers for FormCloseQuery and FormClose. For now, the CloseQuery handler does noting, but I wanted at least to start to incorporate it. Before I tried to test the "Save important parameters to ini file on exit" code, I tested my grasp of the FormCloseQuery and FormClose event handlers with...

procedure Tld010f1.FormCloseQuery(Sender: TObject; var CanClose: boolean);
begin
  if MessageDlg ('Question',
    'Sure you wish to Exit?',
    mtConfirmation,
    [mbYes, mbNo],0) = mrYes
   then CanClose:=true//no ; here
   else CanClose:=false;
end;

procedure Tld010f1.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
  showmessage('Bye');
end;

... and THEN I took remmed out the "Sure?" question, made FormCloseQuery always set CanClose true, and put the "save window size and position to ini file" stuff in the FormClose handler. And tested that much was working.

After it was working, I had the basics for the main ini file in place, and it was then mostly... not entirely... just a tedious session of putting obvious things under the control of the ini file.

The values in the recently created variables sDatePriceFilesPath and sDatePriceFilename go into the general ini file as follows...

sDatePriceFilesPath:=ReadString('DataLocation','Path To Price Files','C:\LTN010Data\Prices\');
  sDatePriceFilename:=ReadString('DataLocation','Initial Stock To Display','ABC');
  //Clean up the path spec. Ensures presence of necessary terminal \
  //"Bug": Will not allow the price files to be in the
  //  root of a storage device, e.g. in C:\\
  //Weakness: Changes  C://Prices/ to C://Prices\ (only changes terminal /)
  while sDatePriceFilesPath[length(sDatePriceFilesPath)]='/' do //no begin here
       sDatePriceFilesPath:=copy(sDatePriceFilesPath,1,length(sDatePriceFilesPath)-1);
  if sDatePriceFilesPath[length(sDatePriceFilesPath)]<>'\' then
       sDatePriceFilesPath:=sDatePriceFilesPath+'\';
  sDatePriceFilename:=sDatePriceFilename+'.txt';//Hardcodes assumption that all
     //of the Date+Price data files are in .txt files... an assumption I can live with.

(And a corresponding block of code... simpler... was added to the FormClose handler, too, of course.)

Ini files for specific stocks...

Right. thinking caps on. the following may seem a little arcane, especially at first.

The easy "what we've got so far" bit:

So far, we have an application which lets us move green lines to where we want them. We've got an application which can load some data from a datafile. The one we're using at the moment is called "ABC.txt". We can change that to a different data file, but for the moment, only by closing the application, editing the general ini file, and then re-starting the application. (We'll add something to ChangeDataStep1 before long, to let the user change which data file is being used.)

Now we're going to create ini files for each of the stocks we are tracking. Why? Because we want somewhere to store WHERE we had the green lines FOR THAT STOCK.

We'll put all of the stock-specific ini files in one folder. We'll store the name of that folder in sStockInifilesPath. They'll be .txt files, and the name for a stock with ticker "ABC" will be "ABCini.txt". So, to keep track of where these files are, we'll add the following to the FormCreate handler....

sStockInifilesPath:=ReadString('DataLocation','StockIniFilesPath',
     'C:\LTN010Data\StockInis\');

(And related code elsewhere)

(To change where the application looks for the stock-specific ini files will require manual editing of the general ini file. Users will rarely have reason to change that setting.)

So... now we have a folder for stock-specific ini files...

Remember: The reason we want a stock-specific ini file for each stock is so we can use it to store where its green lines go.

We've set up sStockInifilesPath, to tell us where those ini files ought to be.

Where the green lines go

We will consult the ini file (or provide for default values, in the absence of one) inside ChangeDataStep1. (We just move the code for setting the iPrevY[] values, which previously took place in the FormCreate handler.)

Re-saving the stock-specific ini files at the right times.

We will have to be careful about where we put the code for writing the green line position information TO the stock specific ini files. (Eventually we may be putting other things in the stock specific ini files, too.)

A SaveStockSpecificIniFile subroutine is indicated!

We will call it during the handling of the FormClose event, of course, but will also call it just before the yet-to-be written "Change the stock we are looking at" code... but there's a little "gotcha" in that... we mustn't do a RE-save stock-specific ini file the first time we execute ChangeDataStep1... because we haven't done a prior ChangeData. So we set up a rather boring boFirstPass boolean, just to take care of this. (See code.)

Change the stock we are looking at.

Again, because the application was BUILT with this feature in mind for the future, it isn't too hard to put it in.

Inside ChangeDataStep1, we have a line which currently says...

sDataSet:='ABC';

"All" we need to do is to change that to...

sDataSet:=InputBox('Choose stock','Enter ticker of stock you want to look at','ABC');

Well... that was almost "all".

In any case, we would have needed to incorporate some "only accept input which specs a ticker for which we have data.

Then, in trying to implement the above, it became clear that there was some duplication of where the program stored "What ticker are we looking at." (The job is shared between sDataSet and sDatePriceFilename... an overlap which should be written out. The fix is probably just a simple matter of changing every instance of sDatePriceFilename into the call of a function which returns sDataSet+'.txt')

AND making the program "play nice", come up showing you whatever stock you were looking at when you last looked, took the introduction of a boFirstPass variable. But if you examine the code, you can see the details of all that.

When you run the code... IF you have the data in C:\LTN010Data\Prices\ABC.txt, you will see the following. You also need a folder called C:\LTN010Data\StockInis, but it can be empty.

-

(The data is just a text file containing...

41746-2300
41745-2280
41744-2270
41743-2265
41742-2270
41741-2245
41740-2198
41739-2198
41738-2195
41737-2190
41736-2180
41735-2155
41734-2158
41733-2165
41732-2175
41731-2180
41730-2175
41729-2180
41728-2175
41727-2180
9999-1000
0000-1000

The following listing now does all that I said I'd do in the tutorial, I think? For a full blown, use it every day, commercial version, you will have to wait until I get that done. It will be shareware... you'll be able to try it before you buy it.

A little bonus... a different resize

For the purpose of the tutorial, what we have is fine. In the real world, it is nice to be able to resize windows to accommodate our wants.

Now- we are already doing one sort of resizing: The size of the graph drawing area can be changed. Eventually, it may be possible to program the application to do that for us, more intuitively.

In this little bonus I am NOT talking about THAT resizing.

When the application's window is in the "restored" state, i.e. not minimized, not maximized, you can move it around the screen, and change its width and height. But, so far, the Quit button and the two scroll bars on the right stay about 345 pixels from the LEFT hand edge of the window, regardless of its size. Crude!

Here's how we can make the quit button and the two right hand scrollbars move with the right hand margin....

Oops! I feel foolish! Accomplishing the above was REALLY easy! I just did "obvious" things to the "Anchors" properties of the three controls. I.e. I changed from the default [akTop, akLeft] to aktop, akRight] by clicking the ellipsis to the right of the Anchors property box in the Object Inspector (f11), and, in the dialog which opened, UNticked "enabled" for Left anchoring, and TICKED the "enabled" box for right anchoring, and "presto", my Quit button and the two right hand scrollbars now maintain the distance from the RIGHT margin, which I set for them by dragging them to where I wanted them on the form.

Oh... and put a...

  sbUpperRight.Left:=ld010f1.width-70;//In FormCreate handler only. Will be okay after
  sbLowerRight.Left:=ld010f1.width-40;//   that due to Anchors property setting.

... in the FormCreate handler, because without it, the scrollbars start at the position, from the LEFT, that they are on your DESIGN window, and stay however far THAT is from the RIGHT edge when you resize the window. You need to START them in the right place (horizontally).

That was so easy, I've been tempted to try to make all the scrollbars taller or shorter, depending on the size of the form....

New variable: wHeightOfScrollbar, type word.

In the FormCreateHandler, initialize wHeightOfScrollbar with function wSetHeightOfScollbar, which takes height of window as a parameter. And set scrollbars to that height... after window size has been determined from ini file. So...

function Tld010f1.wSetHeightOfScollbar(wTmpL:word):word;
begin
  result:=wTmpL-140;
end;

... and....

  wHeightOfScrollbar:=wSetHeightOfScollbar(ld010f1.height);
  sbUpperLeft.Height:=wHeightOfScrollbar;
  sbUpperRight.Height:=wHeightOfScrollbar;
  sbLowerLeft.Height:=wHeightOfScrollbar;
  sbLowerRight.Height:=wHeightOfScrollbar;

... in the FormCreate handler. So far, so good... but scrollbars don't re-size as you change window's size.

Put....

  wHeightOfScrollbar:=wSetHeightOfScollbar(ld010f1.height);
  sbUpperLeft.Height:=wHeightOfScrollbar;
  sbUpperRight.Height:=wHeightOfScrollbar;
  sbLowerLeft.Height:=wHeightOfScrollbar;
  sbLowerRight.Height:=wHeightOfScrollbar;

... in the form's FormResize event handler, and they will, though.

It "should" be a simple matter to put what we have in our Resize Graph's button's Click handler, too, so that the graph area resizes along with the window. I'm going to Wimp Out, and leave that as an exercise for the student. It may "just work"... but we're asking the computer to do an awful lot when we resize the graph, and I just can't face fighting with that just now.

Besides the listing of the final version of the program, below, I also offer a .zip archive with the whole thing, even some suitable "Prices" data files..

Enjoy! I hope in the course my exposition on the creation of that probably not useful to you in that exact form application, you have learned about some techniques which you DID want to know about?

Comments always welcome, nay, desperately yearned for. Please tell me SOMEONE read this??

unit LD010u1;

//NEED TO DEAL WITH...
//   ... if when prgm run, the FOLDER for stock specific inis
//   (or the one for prices, I suspect) does not exist, prgm
//   reacts badly. Nothing "wrecked", but program crashes even before
//   displaying first diagram. There must be a "folder exisits" function
//   to use.
//   In a similar vein... probably behaves badly if prices file doesn't
//      exist for a stock you ask to look for.

(*In trying to implement "change sotck you are showing",
    it became clear that there was some duplication of where the
    program stored "What ticker are we looking at." (The job is shared
    between sDataSet and  sDatePriceFilename... an overlap which should
    be written out... turn sDatePriceFilename into a funtion...
    result:=sDataSet+'.txt')
*)

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls,
  ExtCtrls,IniFiles;

const vers='22 May 14- "Finished"- Tweaked/ tidied';
  //Extension of LD008... a graph drawing application. In this form,
  //   it draws its data from a constant, sDataSource.
  //LDN_010 started 19 May 14, 07:59, an extension of LDN_009, as explained
  //   in LT3_Nf-1 to 3

type TLD010FourCoords = record //no ; here
  x1,y1,x2,y2:integer;
  end;//of declaration of type TLD010FourCoords

type
  { Tld010f1 }
  Tld010f1 = class(TForm)
    buQuit: TButton;
    buResizeIt: TButton;
    buChangeData: TButton;
    eWidth: TEdit;
    eHeight: TEdit;
    Image1: TImage;
    laAppInfo: TLabel;
    Label1: TLabel;
    laTutLink: TLabel;
    laTxtDataSet: TLabel;
    laDataSet: TLabel;
    laTxtWidth: TLabel;
    laTxtHeight: TLabel;
    laVer: TLabel;
    sbUpperRight: TScrollBar;
    sbUpperLeft: TScrollBar;
    sbLowerLeft: TScrollBar;
    sbLowerRight: TScrollBar;
    procedure buChangeDataClick(Sender: TObject);
    procedure buQuitClick(Sender: TObject);
    procedure buResizeItClick(Sender: TObject);
    procedure eWidthChange(Sender: TObject);
    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
    procedure FormCloseQuery(Sender: TObject; var CanClose: boolean);
    procedure FormCreate(Sender: TObject);
    procedure FormResize(Sender: TObject);
    procedure sbUpperLeftScroll(Sender: TObject; ScrollCode: TScrollCode;
      var ScrollPos: Integer);
    procedure sbUpperRightScroll(Sender: TObject; ScrollCode: TScrollCode;
      var ScrollPos: Integer);
    procedure sbLowerLeftScroll(Sender: TObject; ScrollCode: TScrollCode;
      var ScrollPos: Integer);
    procedure sbLowerRightScroll(Sender: TObject; ScrollCode: TScrollCode;
      var ScrollPos: Integer);

  private
    { private declarations }
    bTmp,bGrAxWidX,bGrAxWidY:byte;
    boFirstPass:boolean;
    Bitmap:TBitmap;
    liScYOffset,liPrice:longint;//-2x10^9 to +2x10^9, smallest able to hold
    //the numbers I may present for price-in-pennies. (word can only cope
    //with up to $655.35, (in pennies: 65535)
    iTmp,iImageLeft, iImageTop, iImageWidth, iImageHeight:integer;
    wGrMarBott,wGrMarLeft,
    wGrGAWidth,wGrGAHeight,wGrGAWidthPrev,wGrGAHeightPrev,
    wOldestToDisplay,wDate,wHeightOfScrollbar:word;
    //Dates are held inside this program using the usual Lazarus, and others,
    //convention which made 16 May 2014 41775. 17 May 2014 was 41776, and so on.
    //OpenOffice Calc, and thus I would guess Excel, etc, use the same convention.
    //Under it, Day 1 was December 31 1899
    iGrBelowDataCloudGap:integer;
    sTmp,sDataSet,sIniFileMainName:string;
    wXMaxRaw, wXMinRaw:word;
    liTmp,liTmp2,liYMaxRaw,liYMinRaw,liGrGAMin:longint;
    wScXOffset:word;
    siTmp,siScXStretch,siScYStretch:single;
    bLineBottPercent,bLineTopPercent:byte;
    iGreenLineEnds:array [0..1] of TLD010FourCoords;
    iPrevY:array[0..3] of integer;
    bYStart:array[0..3] of byte;
    //The start positions, y coords of, the four places
    //  green lines cross blue lines, expressed as percents
    //  of whole Y dimension of graph area. N.B. bitmaps and
    //  scrollbars count from 0 at the top, so "10" means
    //  10 percent DOWN from top of graph area.
    //Element 0 for upper left, 1 for upper right,
    //  2 for lower left, 3 for lower right... i.e., as you
    //  would read text.
    //Init'd in FormCreate, eventually to appear in ini file
    //  for the stock in question.
    //these values are of very limited interest they define
    //  the default positions of the GreenLines for a company
    //  which hasn't previously had a GreenLine diagram.
    //Where the green lines are after they've been "started",
    //  which varies from company to company, and will
    //  be sorted in a company specific "ini" file, very soon
    //  become the responsibility of iPrevY[x].
    sDatePriceFilesPath,sDatePriceFilename,
    sStockInifilesPath:string; //The values in sStockInifilesPath and
       //in sDatePriceFilesPath should always end with a \
    procedure EstablishBitmap;
    procedure DrawGraph;
    function iBitMapFrmHumY(iConvert:integer):integer;
    procedure DrawXAxis;
    procedure DrawYAxis;
    procedure MoveLineToAndInclude(iX1,iY1,iX2,iY2:integer);
    procedure PlotDataPoint(wX:word;liY:longint);
    procedure ChangeDataStep1;
    procedure ChangeDataStep2;
    function iRawToGAX(wConvert:word):integer;
    function iRawToGAY(liConvert:longint):integer;
    function iGAtoBitmapX(iConvert:integer):integer;
    function iGAtoBitmapY(iConvert:integer):integer;
    procedure ReadRecord(var w1:word;var li1:longint;var dfDatePriceL:textfile);
    procedure BlueLine(wX:word);
    procedure GreenLine(iX1,iY1,iX2,iY2:integer);
    procedure LayDownGreenLine;
    procedure FillCoords(bWhich:byte;iL0,iL1,iL2,iL3:integer);
    function bPosnAsPercentY(iTmpL:integer):byte;
    function sStockSpecificIniPathAndFileName(sTmpL:string):string;
    procedure SaveStockSpecificIniFile;
    function wSetHeightOfScollbar(wTmpL:word):word;

  public
    { public declarations }
    end;

var
  ld010f1: Tld010f1;

implementation

{$R *.lfm}
{$R+}//Enable range checking

{ Tld010f1 }

function Tld010f1.wSetHeightOfScollbar(wTmpL:word):word;
begin
  result:=wTmpL-140;
end;

procedure Tld010f1.FormCreate(Sender: TObject);
var dfIniFile:TIniFile;
begin
  laVer.caption:='Version '+vers;

  boFirstPass:=true;//Limited role.. but vital.

  sIniFileMainName:='LT010ini.txt';

  dfIniFile:=TIniFile.Create(sIniFileMainName);

  with dfIniFile do begin

  //Set posn and size of application's window...
  ld010f1.top:=ReadInteger('WindowPosnNSize','Top',100);
  ld010f1.left:=ReadInteger('WindowPosnNSize','Left',10);
  ld010f1.height:=ReadInteger('WindowPosnNSize','Height',300);
  ld010f1.width:=ReadInteger('WindowPosnNSize','Width',435);

  //Variables related to the TImage object...
  //See https://sheepdogguides.com/lut/lt1Graphics.htm for details
  //  of using a bitmap as a drawing surface to work with...
  iImageLeft:=ReadInteger('ImagePosnNSize','Left',60);
  iImageTop:=ReadInteger('ImagePosnNSize','Top',120);
  iImageWidth:=ReadInteger('ImagePosnNSize','Width',280);
  iImageHeight:=ReadInteger('ImagePosnNSize','Height',150);

  eWidth.text:=inttostr(iImageWidth);
  eHeight.text:=inttostr(iImageHeight);

  sDatePriceFilesPath:=ReadString('DataLocation','Path To Price Files','C:\LTN010Data\Prices\');
  //Now clean up the path spec. Ensures presence of necessary terminal \
  //"Bug": Will not allow the price files to be in the
  //  root of a storage device, e.g. in C:\\
  //Weakness: Changes  C://Prices/ to C://Prices\ (only changes terminal /)
  while sDatePriceFilesPath[length(sDatePriceFilesPath)]='/' do //no begin here
       sDatePriceFilesPath:=copy(sDatePriceFilesPath,1,length(sDatePriceFilesPath)-1);
  if sDatePriceFilesPath[length(sDatePriceFilesPath)]<>'\' then
       sDatePriceFilesPath:=sDatePriceFilesPath+'\';

  sDatePriceFilename:=ReadString('DataLocation','Initial Stock To Display','ABC');
  sDataSet:=sDatePriceFilename;//sDataSet we will hold the name MINUS the .txt
  sDatePriceFilename:=sDatePriceFilename+'.txt';//Hardcodes assumption that all
     //of the Date+Price data files are in .txt files... an assumption I can live with.
     //Instance "two" of at least two instances of this.

  sStockInifilesPath:=ReadString('DataLocation','StockIniFilesPath',
     'C:\LTN010Data\StockInis\');
  while sStockInifilesPath[length(sStockInifilesPath)]='/' do //no begin here
       sStockInifilesPath:=copy(sStockInifilesPath,1,length(sStockInifilesPath)-1);
  if sStockInifilesPath[length(sStockInifilesPath)]<>'\' then
       sStockInifilesPath:=sStockInifilesPath+'\';

  end;//with...

  dfIniFile.Free;

  wHeightOfScrollbar:=wSetHeightOfScollbar(ld010f1.height);
  sbUpperLeft.Height:=wHeightOfScrollbar;
  sbUpperRight.Height:=wHeightOfScrollbar;
  sbLowerLeft.Height:=wHeightOfScrollbar;
  sbLowerRight.Height:=wHeightOfScrollbar;

  sbUpperRight.Left:=ld010f1.width-70;//In FormCreate handler only. Will be okay after
  sbLowerRight.Left:=ld010f1.width-40;//   that due to Anchors property setting.

  bGrAxWidX:=1;
  bGrAxWidY:=1;

  wGrMarBott:=4;
  wGrMarLeft:=6;

  bLineBottPercent:=10;// Set these to 10 and 80
  bLineTopPercent:=80;//  respectively, and the blue line
  //  will run from 10 percent up the GraphArea to 80
  //  percent up the GraphArea

  //Establish some initial "Previous" values. "Current" will be set
  //  in a more relevant place.
  //Can, in FormCreate, use these as "current"

  wGrGAWidthPrev:=iImageWidth-wGrMarLeft-bGrAxWidY;
  wGrGAHeightPrev:=iImageHeight-wGrMarBott-bGrAxWidX;

  ChangeDataStep1;//Must happen BEFORE EstablishBitMap can occur
  EstablishBitmap;//Must happen AFTER core variables have values
  //ChangeDataStep2 is executed inside EstablishBitMap

  DrawGraph;

end;// of FormCreate

procedure Tld010f1.FormResize(Sender: TObject);
begin
  wHeightOfScrollbar:=wSetHeightOfScollbar(ld010f1.height);
  sbUpperLeft.Height:=wHeightOfScrollbar;
  sbUpperRight.Height:=wHeightOfScrollbar;
  sbLowerLeft.Height:=wHeightOfScrollbar;
  sbLowerRight.Height:=wHeightOfScrollbar;
end;

procedure Tld010f1.EstablishBitmap;
//A purist would pass things like iImageWidth to this procedure
//   as parameters. I am "breaking the rule" deliberately,
//   judging the value, here, not worth the nuisance.
//Besides Establishing the bitmap, this routine changes the
//   values in some variables referred to elsewhere when
//   scaling things to fit graph well.

var siVert:single;

begin  //EstablishBitmap

  Image1.left:=iImageLeft;//Set size and posn of TImage control
  Image1.top:=iImageTop;
  Image1.width:=iImageWidth;
  Image1.height:=iImageHeight;

  Bitmap:=TBitmap.create;//Create a bitmap object

  Bitmap.width:=iImageWidth;//Adjust dimensions
  Bitmap.height:=iImageHeight;

  //In Delphi, bitmap background is white to begin with. Black in Lazarus
  Bitmap.canvas.pen.color:=clWhite;//1 of 2 lines not needed in Delphi...
  Bitmap.canvas.Rectangle(0,0,iImageWidth,iImageHeight);//2 of 2 not needed, Delphi

  //... continue EstablishBitmap....
  Image1.Picture.Graphic:=Bitmap; //Assign the bitmap to the image component
  //Note how we have adjusted the size of the image to the size of the bitmap
  //   we will be assigning to it.

  //Now fill two variables with values for the width and height of the
  //  "Graph Area", i.e. that part of the bitmap to the left of, and
  //  above the two axes.
  //If there are 15 rows of pixels and and 25 columns of pixels in the
  //  Graph Area, then you would set wGrGAWidth:=15, wGrGAHeight:=25

  wGrGAWidth:=iImageWidth-wGrMarLeft-bGrAxWidY;
  wGrGAHeight:=iImageHeight-wGrMarBott-bGrAxWidX;

  //... continue EstablishBitmap....

  //IS THIS REALLY THE PLACE FOR THESE??? (Belive yes at 23 May 14 when
  //  after they have been here for some time.
  //Seems to work... but not carefully thought out.
  //If it doesn't work... is it the the placement of these lines,
  //  or is it how they are used? or how things are done
  //  elsewhere?...
  sbUpperLeft.max:=wGrGAHeight;
  sbUpperLeft.min:=0;
  sbLowerLeft.max:=wGrGAHeight;
  sbLowerLeft.min:=0;
  sbUpperRight.max:=wGrGAHeight;
  sbUpperRight.min:=0;
  sbLowerRight.max:=wGrGAHeight;
  sbLowerRight.min:=0;

 //... continue EstablishBitmap....
 siVert:=wGrGAHeight/wGrGAHeightPrev; //Adjustment factor, to take us across Resize

 iPrevY[0]:=trunc(iPrevY[0]*siVert);
 iPrevY[1]:=trunc(iPrevY[1]*siVert);
 iPrevY[2]:=trunc(iPrevY[2]*siVert);
 iPrevY[3]:=trunc(iPrevY[3]*siVert);

 Label1.caption:='';
 iTmp:=iPrevY[0];//Bitmap AND scrollbar: min at TOP, max at bottom
 sbUpperLeft.position:=iTmp;

 iTmp:=iPrevY[1];
 sbUpperRight.position:=iTmp;

 iTmp:=iPrevY[2];
 sbLowerLeft.position:=iTmp;

 iTmp:=iPrevY[3];
 sbLowerRight.position:=iTmp;

  //Record these for use across next resize event
  wGrGAHeightPrev:=wGrGAHeight;
  wGrGAWidthPrev:=wGrGAWidth;

  //At this point, ChangeDataStep2 must be called to recalc the scaling
  //  factors, but ChangeDataStep1 doesn't need to be called every
  //  time we are doing EstablishBitmap because sometimes
  //  we are still dealing with the same data, and any data changing
  //  or assessing the range of values in the data takes place in Pt1
  ChangeDataStep2;

end; // of EstablishBitmap

procedure Tld010f1.buResizeItClick(Sender: TObject);
begin
  Bitmap.destroy;

  iImageWidth:=strtoint(eWidth.text);
  iImageHeight:=strtoint(eHeight.text);

  //ChangeDataStep1 not needed JUST to resize.
  EstablishBitmap;
  //ChangeDataStep2; This happens inside EstablishBitmap.
  DrawGraph;
end;

procedure Tld010f1.eWidthChange(Sender: TObject);
begin

end;

procedure Tld010f1.FormCloseQuery(Sender: TObject; var CanClose: boolean);
begin
  {if MessageDlg ('Question',
    'Sure you wish to Exit?',
    mtConfirmation,
    [mbYes, mbNo],0) = mrYes
   then }CanClose:=true{//no ; here
   else CanClose:=false};
end;

procedure Tld010f1.PlotDataPoint(wX:word;liY:longint);
//This procedure accesses external "constants".
//X was put in a word-type variable, and Y in a longint
//   because of the data anticipated in the specific
//   application this was written for. (Word wasn't big
//   enough for some of the Y data.)
var iTmpXL,iTmpYL:integer;
begin
  iTmpXL:=iGAtoBitmapX(iRawToGAX(wX));
  iTmpYL:=iGAtoBitmapY(iRawToGAY(liY));
  Image1.Picture.Bitmap.canvas.pixels
    [iTmpXL,iTmpYL]:=Image1.Picture.Bitmap.canvas.pen.color;
end;//of PlotDataPoint

procedure Tld010f1.DrawGraph;
var dfDatePrice:textfile;

begin
 Image1.Picture.Bitmap.canvas.pen.color:=clBlack;

 DrawXAxis;
 DrawYAxis;

//Connect to data stream
//Start of code which is very nearly repeated in DrawGraph
//"Connect" to data, and put pointer to "what record we want to read
//    next to first record.

sTmp:=sDatePriceFilesPath+sDatePriceFilename;
//e.g. C:\LTN010Data\Prices\ABC.txt

//Datastream is already validated... happened back in
//  ChangeDataStep1 before scanning for max/mins

assignfile(dfDatePrice,sTmp);
reset(dfDatePrice);//Opens for read

//End of code which is very nearly repeated in DrawGraph

 ReadRecord(wDate,liPrice,dfDatePrice);//"Priome pump" for repeat loop
 repeat
   PlotDataPoint(wDate,liPrice);
   ReadRecord(wDate,liPrice,dfDatePrice);
 until {out of data or}wDate<wOldestToDisplay;

 //Release connection to datastream will go here, when one needed
 closefile(dfDatePrice);

 // ... continue DrawGraph ....

 BlueLine(41730);//Need to make fancier, tie to x1, x2.
 BlueLine(41750);

//Put GreenLines in start posns...

iTmp:=sbUpperRight.position;

//The "10" and "90" need to go into globals.... (space, left and right, around GreenLine)
//(The horizontal extent of the green lines is pretty rough and ready at the
//      moment. Fancier rules could be created, implemented.)

//A function is needed... something very like...
//   wGrMarLeft+bGrAxWidX+trunc(wGrGAWidth*10/100)
//   ... which turns up in too many places. (The 10 is, at present, sometimes a 90)
FillCoords(0,wGrMarLeft+bGrAxWidX+trunc(wGrGAWidth*10/100),
   iPrevY[0],wGrMarLeft+bGrAxWidX+trunc(wGrGAWidth*90/100),iTmp);
FillCoords(1,wGrMarLeft+bGrAxWidX+trunc(wGrGAWidth*10/100),
   iPrevY[2],wGrMarLeft+bGrAxWidX+trunc(wGrGAWidth*90/100),iPrevY[3]);

GreenLine(
    iGreenLineEnds[0].x1,iGreenLineEnds[0].y1,
    iGreenLineEnds[0].x2,iGreenLineEnds[0].y2);
    iPrevY[1]:=iGreenLineEnds[0].y2;

GreenLine(
    iGreenLineEnds[1].x1,iGreenLineEnds[1].y1,
    iGreenLineEnds[1].x2,iGreenLineEnds[1].y2);

end;//of DrawGraph

procedure Tld010f1.FillCoords(bWhich:byte;iL0,iL1,iL2,iL3:integer);
begin
 iGreenLineEnds[bWhich].x1:=il0;;
 iGreenLineEnds[bWhich].y1:=il1;;
 iGreenLineEnds[bWhich].x2:=il2;;
 iGreenLineEnds[bWhich].y2:=il3;
end;//of FillCoords

//======
(*
function Tld010f1.iScaleX(iConvert:integer):integer;
//ver 13 May 14, 16:18... see LT3Nf-1.htm, if you need to alter.
//This no longer has a role in the code, but remains because
//  reference is made to a "iScaleX" function in LT3N-1.htm, a
//  tutorial explaining the development of this application.
begin
  siTmp:=(iConvert-iScXOffset)*siScXStretch;
  iTmp:=trunc(siTmp);
  result:=iTmp;
end;//iScaleX
*)
//=========

//==============================
procedure Tld010f1.DrawXAxis;
//Could be made more clear? Use wGrGAHeight/wGrGAWidth,
//    or just work lineto in Bitmap units?
//Re-work DrawYAxis at same time
var bTmpL:byte;
begin
  for bTmpL:=0 to bGrAxWidX-1 do begin
  Image1.Picture.Bitmap.canvas.moveto
    (iGAtoBitmapX(-1),  iBitMapFrmHumY(-1-bTmpL));

  Image1.Picture.Bitmap.canvas.lineto
    (iImageWidth,       iBitMapFrmHumY(-1-bTmpL));
  end;//for
end; //DrawXAxis

procedure Tld010f1.DrawYAxis;
//Could be made more clear? Use wGrGAHeight/wGrGAWidth,
//    or just work lineto in Bitmap units?
//Re-work DrawXAxis at same time
var bTmpL:byte;
begin
  for bTmpL:=0 to bGrAxWidY-1 do begin
  Image1.Picture.Bitmap.canvas.moveto
    (iGAtoBitmapX(-1-bTmpL), iBitMapFrmHumY(-1)+bGrAxWidX-1);

  Image1.Picture.Bitmap.canvas.lineto
    (iGAtoBitmapX(-1-bTmpL),  -1);
  end; //for
end; //DrawYAxis

//=====================
procedure Tld010f1.MoveLineToAndInclude(iX1,iY1,iX2,iY2:integer);
//If you call lineto(0,0,3,3), only pixels 0,0/ 1,1/ and 2,2 are
//  affected. This procedure, when called with the same parameters
//  affects all of those pixels AND pixel 3,3.
begin
  Image1.Picture.Bitmap.canvas.moveto(iGAtoBitmapX(iX1),iBitMapFrmHumY(iY1));
  Image1.Picture.Bitmap.canvas.lineto(iGAtoBitmapX(iX2),iBitMapFrmHumY(iY2));
  Image1.Picture.Bitmap.canvas.pixels[iGAtoBitmapX(iX2),
                    iBitMapFrmHumY(iY2)]:=Image1.Picture.Bitmap.canvas.pen.color;
end; //MoveLineToAndInclude

procedure Tld010f1.buQuitClick(Sender: TObject);
begin
  close;
end;

procedure Tld010f1.buChangeDataClick(Sender: TObject);
begin
  ChangeDataStep1;
  EstablishBitmap;//to clear previous data from graph
  //ChangeDataStep2; Is done inside EstablishBitmap;
  DrawGraph;
end;

function Tld010f1.iBitMapFrmHumY(iConvert:integer):integer;
//ver 13 May 14, 16:18... see LT3Nf-1.htm, if you need to alter.
//To be written out, replaced by iGAtoBitmapY. Make note in
//   LT3Nf-1 at place where iBitMap... was developed when it is
//   written out.
begin
  //With iImageHeight=20, wGrMarBott=4, bGrAxWidY=1, the number that
  //  iConvert is taken away from should be 14.
  //The brackets aren't necessary for the mathematics, but are there
  //  to help you see the basic "trick" of inverting a series of
  //  numbers: take a varying but increasing "input" number away
  //  from a constant to obtain a varying but DEcreasing "output".
  result:=wGrGAHeight-iConvert;
end;

procedure Tld010f1.ChangeDataStep1;
//Establish which data stream is to be read, and "connect" to it.
//Do an analysis of the data, determine max and min X and Y
//Update on-screen display naming the data set we are using.

var dfDatePrice:textfile;
    dfIniFile:TIniFile;

begin //ChangeDataStep1

  //We need to do a SaveStockSpecificIniFile... UNLESS this is the FIRST
  //  call of ChangeDataStep1 (which happens before user input when
  //  program is started. (We don't have data to save yet, in THAT pass.)
  if not(boFirstPass) then begin
     SaveStockSpecificIniFile;

     repeat
     sDataSet:=InputBox('Choose stock','Enter ticker of stock you want to look at','ABC');
     until true;//Need to be sure a SENSIBLE value (prices file avail)
        //has been entered for sDataSet

     sDatePriceFilename:=sDataSet+'.txt';//Hardcodes assumption that all
     //of the Date+Price data files are in .txt files... an assumption I can live with.
     //Instance "one" of at least two instances of this.


  end;//if not(boFirstPass...
  boFirstPass:=false;

  laDataSet.caption:=sDataSet;//Tell the user what data we are reading.
  //sDataSet IS used for more than just this!

  //The application is set up to scan data files, which must be in
  //  chronological order, oldest first. The application will not
  //  always read ALL of the data available. It will only scan back
  //  as far as it has been told to by the value in wOldestToDisplay,
  //  which users will, one day, be able to change as they wish.

  //==================             (continue ChangeDataStep1... )
  //Having designated a datafile to read, the app then looks to see
  //  if there is a stock-specific ini file for it. Certain stock-specific
  //  values are either fetched from the ini file, or the defaults in
  //  the following are applied.....

  dfIniFile:=TIniFile.Create(sStockSpecificIniPathAndFileName(sDataSet+'ini'));

  with dfIniFile do begin
    //Prepare to plot green lines where they were last plotted
    //iPrev values are only right for a given scaling, i.e. after a
    //  a (re-)size the same as when first drawn... but shouldn't(?)
    //  be a problem.. The re-calcs of iPrev will(?) always have
    //  happened, to match re-sizings, before we access a stock-
    //  specific ini file?? Or does the size the iPrevs were done
    //  for need to be in stock-specific ini file, and acted on?

    //bYStart: Values have SMALL role: Determine green line end posns
    //  for a stock with no previous entries in stock-specific ini file
    bYStart[0]:=40;//Percents of whole. See notes where array declared.
    bYStart[1]:=10;
    bYStart[2]:=55;
    bYStart[3]:=50;

    iPrevY[0]:=ReadInteger('GreenLine','UpperLeft',trunc(wGrGAHeightPrev*bYStart[0]/100));
    iPrevY[1]:=ReadInteger('GreenLine','UpperRight',trunc(wGrGAHeightPrev*bYStart[1]/100));
    iPrevY[2]:=ReadInteger('GreenLine','LowerLeft',trunc(wGrGAHeightPrev*bYStart[2]/100));
    iPrevY[3]:=ReadInteger('GreenLine','LowerRight',trunc(wGrGAHeightPrev*bYStart[3]/100));

    end;//with...

  dfIniFile.Free;
  //(end of "fetch stuff from stock specific ini file/ use defaults")

  //Set Scrollbar.postions accordingly, as lines draw from these
  //   initially (at least)

  sbUpperLeft.position:=iPrevY[0];
  sbUpperRight.position:=iPrevY[1];
  sbLowerLeft.position:=iPrevY[2];
  sbLowerRight.position:=iPrevY[3];

  wOldestToDisplay:=41729;

  // ... continue ChangeDataStep1...
  //Start of code which is very nearly repeated in DrawGraph
  //"Connect" to data, and put pointer to "what record we want to read
  //    next to first record.

  sTmp:=sDatePriceFilesPath+sDatePriceFilename;
  //e.g. Something like.... C:\LTN010Data\Prices\ABC.txt
  //  On the first pass thorough the application, the default values will
  //      trace back to entries in the general ini file.

  if not(fileexists(sTmp)) then begin
     showmessage('File not found. App will close. Will need to be fancier, eventually.');
     application.terminate;
     end;

  assignfile(dfDatePrice,sTmp);
  reset(dfDatePrice);//Opens for read

  //End of code which is very nearly repeated in DrawGraph
  //Put that "prep to scan data" code in a SR?

  //Close and re-open datastream between "Scan to find max/min" and "scan to plot"

  //Give maxima and minima initial values.
  wXMinRaw:=65533;
  wXMaxRaw:=0;
  liYMinRaw:=99999;
  liYMaxRaw:=-5000000;

  //Scan the data, determine max/min X&Y, though oldest date of interest

  ReadRecord(wDate,liPrice,dfDatePrice);//"Prime pump" for "repeat" loop

  //if invalid(ior wDate<wOldestToDisplay, report and abort rest. Else...
  begin// "1"
    repeat
      if wDate<wXMinRaw then wXMinRaw:=wDate;
      if wDate>wXMaxRaw then wXMaxRaw:=wDate;
      if liPrice<liYMinRaw then liYMinRaw:=liPrice;
      if liPrice>liYMaxRaw then liYMaxRaw:=liPrice;
      ReadRecord(wDate,liPrice,dfDatePrice);
      //showmessage('JUST READ:'+inttostr(wDate)+'   OLDEST TO
DISPLAY:'+inttostr(wOldestToDisplay));
    until {file is empty or} wDate<wOldestToDisplay;
  end;//begin "1"

  //Release connection to datastream...
  closefile(dfDatePrice);
end;//of ChangeDataStep1

//=====================
//Raw data to GraphArea coordinate conversion functions....

function Tld010f1.iRawToGAX(wConvert:word):integer;
//Believed completely "sound".
begin
  result:=trunc((wConvert-wScXOffset)*siScXStretch);
  end;

function Tld010f1.iRawToGAY(liConvert:longint):integer;
//This will do offset and scale but NOT the inversion which the
//  "upside down" Y axis of the bitmap demands. That's taken care
//  of withing iGAtoBitmap. The numbers from iRawToGAY run from
//  zero to wGrGAHeight (-1) (or not.. there may be a "off-by-one"
//  problem lurking here... change calc of scale factor, if so,
//  but as I plan some whitespace above and below, this "isn't"(?)
//  a major problem?
//NOTE: There are strange things going on, because of the inverted
//  scale... fiddle carefully, if you must do it at all!
begin
  result:=trunc((liConvert-liYMinRaw)*siScYStretch);
  end;

//================
function Tld010f1.iGAtoBitmapX(iConvert:integer):integer;
//GraphArea to Bitmap coordinate conversion (X)
begin
  result:=iConvert+wGrMarLeft+bGrAxWidY;
  end;

function Tld010f1.iGAtoBitmapY(iConvert:integer):integer;
//GraphArea to Bitmap coordinate conversion (Y)
//Remember: Bitmap "Y" coord has zero at TOP. The values
//  passed to this function will be "right", but, in bitmap
//  Y coords terms, "upside down".
//Happily, because, once in bitmap coords, we are working
//  from the top down, we don't have to worry about the
//  axis line and the bottom margin... they are "above" the
//  datapoints we want to put on bitmap.
begin
  result:=(wGrGAHeight-iConvert-1)-iGrBelowDataCloudGap;
  //The "-1" is a KLUDGE! Fixes "show the thing that should
  //  be at the bottom of the graph...
  //  But may push "thing that should be at top" over the edge,
  //    past the top of the Bitmap.
  //As I intend having some "white space" above and below
  //  the plotted data points, I can, though I shouldn't,
  //  live with this... IF it is happening. It may be
  //  that max values plot properly, i.e. in top row, too.
  //The MINUS iGrBelowDataCloudGap is to create the blank
  //  area below the data cloud. It has to be minus as the
  //  coordinates of the bitmap go up as you go down the screen.
  end;  //iGAtoBitmapY

//============
procedure Tld010f1.ReadRecord(var w1:word;var li1:longint;var dfDatePriceL:textfile);
//w1, li1 can have anything in them when the SR is called. Irrelevant.
//wWhereInData must have a correct value. Should be 1 when ReadRecord
//    first called
//Pick up two numbers from global constant sDataSource.
//(Using globals, the way wWhereInData and sDataSource are being used is
//    a Bad Idea... and I will probably be punished. We shall see.)
//The repeated transfer of dfDatePrice to dfDatePriceL may impose a time
//    penalty which makes it worth using a global variable there.
//BE CAREFUL NOT TO (overtly) CHANGE THE VALUE IN dfDatePriceL.... file
//    handle must be passed as VAR type parameter. Probably holds the
//    "data read so far" pointer within it.
begin
  readln(dfDatePriceL,sTmp);
  //showmessage(sTmp);

  bTmp:=pos('-',sTmp);
  //showmessage(inttostr(bTmp));

  w1:=strtoint(copy(sTmp,1,bTmp-1));

  li1:=strtoint(copy(sTmp,bTmp+1,length(sTmp)-bTmp));
  //showmessage(inttostr(w1)+'  --  '+inttostr(li1));

end;//ReadRecord

procedure Tld010f1.GreenLine(iX1,iY1,iX2,iY2:integer);
//Parameters expressed in the units used by the bitmap.
//Remember: Draws TO, but not including, iX2,iY2
begin
  Image1.Picture.Bitmap.canvas.pen.mode:=pmXor;
  Image1.Picture.Bitmap.canvas.pen.color:=(clGreen XOR clWhite);//changes
        //white to green and visa versa when line drawn, etc.

  Image1.Picture.Bitmap.canvas.moveto(iX1,iY1);
  Image1.Picture.Bitmap.canvas.lineto(iX2,iY2);

  Image1.Picture.Bitmap.canvas.pen.mode:=pmCopy;
end;// (draw...) GreenLine

procedure Tld010f1.BlueLine(wX:word);
//Set boDraw true to draw the line, false to erase it
//This can almost certainly be made to run more quickly
//  by pre-calculating elsewhere the Y values for the
//  ends of the blue line
var iLineBottBitmap, iLineTopBitmap:integer;//The Y coords, in
    //"bitmap" units, for top and bottom of the line.
begin
//Figure out where the bottom of the line will be
siTmp:=(wGrGAHeight*bLineBottPercent)/100;
iLineBottBitmap:=wGrGAHeight-trunc(siTmp);

//Figure out where the top of the line will be
siTmp:=(wGrGAHeight*(100-bLineTopPercent))/100;
iLineTopBitmap:=trunc(siTmp);

Image1.Picture.Bitmap.canvas.pen.mode:=pmXor;//pmCopy seems to be the default
Image1.Picture.Bitmap.canvas.pen.color:=(clBlue XOR clWhite);//changes
        //white to blue and visa versa when line drawn, etc.

Image1.Picture.Bitmap.canvas.moveto(iGAtoBitmapX(iRawToGAX(wX)),
                 iLineBottBitmap);
Image1.Picture.Bitmap.canvas.lineto(iGAtoBitmapX(iRawToGAX(wX)),
                 iLineTopBitmap);

Image1.Picture.Bitmap.canvas.pen.mode:=pmCopy;
end;// (draw...) BlueLine

procedure Tld010f1.LayDownGreenLine;
//This procedure puts a GreenLine on the bitmap for
//  the first time after bitmap wiped clear for new start
//  or after resize. (Etc?)
//Calls of LayDownGreenLine must be inserted in just
//  the right places, and only there.
//The first one was put near the end of DrawGraph,
//  which lays down the data points.
begin
iTmp:=sbUpperRight.position;
GreenLine(wGrMarLeft+bGrAxWidX+trunc(wGrGAWidth*10/100),
       20,wGrMarLeft+bGrAxWidX+trunc(wGrGAWidth*90/100),iTmp);
end;

function Tld010f1.bPosnAsPercentY(iTmpL:integer):byte;
begin
result:=trunc(iTmpL/wGrGAHeight)*100;
end;

procedure Tld010f1.ChangeDataStep2;
//Much (all?) of this is concerned with (re)calculating constants for
//  converting Raw data to GA data and GA to bitmap coordinates.

//Work out the scaling factors needed for plotting, using
//  the current display area information (from variables)
//  and the current data set's characteristics (max and
//  min X and Y.

//The idea of the offsets and streches are to map data from
//  ranges defined by the Min/Max variables filled in ChangeDataStep1
//  to ranges which suit our graphing needs better... from 0 to the
//  maximum X or Y coordinate, as expressed in pixels.
//If, on say the Y axis, our data went from 10 to 110, and we had
//  pixels 0 to 10 available to display that data, the offset would
//  be 10, to bring 10 to 110 down to 0 to 100, and the scale factor
//  would be 0.1 to bring the 0 to 100 down to 0 to 10.
//The first row (or colum) of pixels in the display area will be
//  row or column "=0", at least as far as the plotting routines are
//  concerened. It can STAND FOR anything, of course, standing for
//  10 in the example developed above.
var wFakeXMax:word;
    liFakeYMin,liFakeYMax:longint;//These variables are not
    //really necessary, but I hope they help make more clear the
    //logic of how the blank areas on the graph are created.
    bGrGapRight,bGrGapAbove,bGrGapBelow:byte;

begin  //ChangeDataStep2
  //Fill the relevant variables with the values which determine
  //  the size of the blank areas above, below, and to the
  //  right of the cloud of plotted data points.
  //These numbers are "percents", in other words, if
  //  bGrGapRight is set to 40, then the right hand 40% of
  //  the width of the GraphArea should be blank.
  bGrGapRight:=40;
  bGrGapAbove:=40;
  bGrGapBelow:=30;

//Deal with creating blank area to right of data cloud
//Everything relevant is here.
  siTmp:=(wXMaxRaw-wXMinRaw)/(100-bGrGapRight);
  iTmp:=trunc(siTmp*bGrGapRight);
  wFakeXMax:=wXMaxRaw+iTmp;

//...continue ChangeDataStep2...
//Deal with creating blank areas above and below data cloud.
//Most of what is relevant here, and a slight change
//  was also needed in iGAtoBitmapY. The "-iGrBelowDataCloudGap"
//  term is about creating the bottom gap.

  liTmp:=liYMaxRaw-liYMinRaw;
  siTmp:=liTmp/(100-bGrGapAbove-bGrGapBelow);
  liTmp:=trunc(siTmp*bGrGapAbove);
  liTmp2:=trunc(siTmp*bGrGapBelow);

  liFakeYMax:=liYMaxRaw+liTmp;
  liFakeYMin:=liYMinRaw-liTmp2;
  liGrGAMin:=liFakeYMin;//Need elsewhere (DrawBlue, for instance)

  wScXOffset:=wXMinRaw;
  liScYOffset:=liFakeYMax;

  siScXStretch:=wGrGAWidth/(wFakeXMax-wXMinRaw+1);//The "+1" is a KLUDGE...
  //with it, the range isn't stretched TOO far. (Without it, the last
  //data point is off the edge (or top... bottom(?) of page.)

  siScYStretch:=wGrGAHeight/(liFakeYMax+1-liFakeYMin);//The "+1" is a KLUDGE.

  siTmp:=(wGrGAHeight*bGrGapBelow)/100;
  iGrBelowDataCloudGap:=trunc(siTmp);

end; //ChangeDataStep2

procedure Tld010f1.sbUpperRightScroll(Sender: TObject; ScrollCode: TScrollCode;
  var ScrollPos: Integer);
//(Upper_Right was first one I did. Took forever, c. 18May14)
//You must ensure that the line has been drawn ONCE before this is called
//   for the first time.
//iPrevX[n] and iPrevY[n] tell you where the line lies, as you enter this
//   "redraw-line-in-new-posn routine. You need to know, so you can, as
//   part of this, erase old line first, using the wonders of page mode pmXOR.
//You must not call Greenline elsewhere, unless you call it exactly a
//  multiple of two times, duplicating the erase/redraw sequence of the
//  following, and keeping iPrevY up to date.

var wTmpYL:word;

begin  //sbUpperRightScroll
  wTmpYL:=ScrollPos;//Same... I think!... as sbUpperRight.position, but, within
     //this subroutine, more accessible, I hope.
     //Make note for erase-in-future of where new line was
  label1.caption:=inttostr(wTmpYL);

  //Overdraw old line, erasing it, in prep for draw new line...
  //(It may be worth, for speed's sake, eventually redoing as...
  //   GreenLine(0,10,60,150,iPrevY[1]);, skipping the
  //         FillCoords/ 2x access to iGreenLineEnds(record)
  //But wait to see final form with all coords flexible before
  //   doing that.

  FillCoords(0,wGrMarLeft+bGrAxWidX+trunc(wGrGAWidth*10/100),
     iPrevY[0],wGrMarLeft+bGrAxWidX+trunc(wGrGAWidth*90/100),iPrevY[1]);
                                            //sbUpperRightScroll continued....

  //Overdraw old, erasing it...
      GreenLine(
         iGreenLineEnds[0].x1,iPrevY[0],
         iGreenLineEnds[0].x2,iPrevY[1]);

  //Draw new...
      GreenLine(
         iGreenLineEnds[0].x1,iPrevY[0],
         iGreenLineEnds[0].x2,wTmpYL);

  //Record where end of new was, for next erase...
      iPrevY[1]:=wTmpYL;

end;//sbUpperRightScroll

procedure Tld010f1.sbUpperLeftScroll(Sender: TObject; ScrollCode: TScrollCode;
  var ScrollPos: Integer);
//See notes at Upper_Right... works in similar way.
//Consider collapsing these three event handlers into a single event handler
//   which can handle all four srcoll bar "scroll" events. For "tidiness" and
//   the challenge of it. Not "needed".

var wTmpYL:word;

begin  //sbUpperLeftScroll
  wTmpYL:=ScrollPos;
     //Make note for erase-in-future of where new line was
  label1.caption:=inttostr(wTmpYL);

  //Overdraw old line, erasing it, in prep for draw new line...
  //(It may be worth, for speed's sake, eventually redoing as...
  //   GreenLine(0,10,60,150,iPrevY[1]);, skipping the
  //         FillCoords/ 2x access to iGreenLineEnds(record)
  //But wait to see final form with all coords flexible before
  //   doing that.

  FillCoords(0,wGrMarLeft+bGrAxWidX+trunc(wGrGAWidth*10/100),
     iPrevY[0],wGrMarLeft+bGrAxWidX+trunc(wGrGAWidth*90/100),iPrevY[1]);
                                            //sbUpperLeftScroll continued....

  //Overdraw old, erasing it...
      GreenLine(
         iGreenLineEnds[0].x1,iPrevY[0],
         iGreenLineEnds[0].x2,iPrevY[1]);

  //Draw new...
      GreenLine(
         iGreenLineEnds[0].x1,wTmpYL,
         iGreenLineEnds[0].x2,iPrevY[1]);

  //Record where end of new was, for next erase...
      iPrevY[0]:=wTmpYL;

end;//sbUpperLeftScroll

procedure Tld010f1.sbLowerRightScroll(Sender: TObject; ScrollCode: TScrollCode;
  var ScrollPos: Integer);
//See notes at Upper_Right... works in similar way.

var wTmpYL:word;

begin  //sbLowerRightScroll
  wTmpYL:=ScrollPos;
     //Make note for erase-in-future of where new line was
  label1.caption:=inttostr(wTmpYL);

  //Overdraw old line, erasing it, in prep for draw new line...
  //(It may be worth, for speed's sake, eventually redoing as...
  //   GreenLine(0,10,60,150,iPrevY[1]);, skipping the
  //         FillCoords/ 2x access to iGreenLineEnds(record)
  //But wait to see final form with all coords flexible before
  //   doing that.

  FillCoords(1,wGrMarLeft+bGrAxWidX+trunc(wGrGAWidth*10/100),
     iPrevY[2],wGrMarLeft+bGrAxWidX+trunc(wGrGAWidth*90/100),iPrevY[3]);
                                            //sbLowerRightScroll continued....

  //Overdraw old, erasing it...
      GreenLine(
         iGreenLineEnds[1].x1,iPrevY[2],
         iGreenLineEnds[1].x2,iPrevY[3]);

  //Draw new...
      GreenLine(
         iGreenLineEnds[1].x1,iPrevY[2],
         iGreenLineEnds[1].x2,wTmpYL);

  //Record where end of new was, for next erase...
      iPrevY[3]:=wTmpYL;

end;//sbLowerRightScroll


procedure Tld010f1.sbLowerLeftScroll(Sender: TObject; ScrollCode: TScrollCode;
  var ScrollPos: Integer);
//See notes at Upper_Right... works in similar way.

var wTmpYL:word;

begin  //sbLowerLeftScroll
  wTmpYL:=ScrollPos;
     //Make note for erase-in-future of where new line was
  label1.caption:=inttostr(wTmpYL);

  //Overdraw old line, erasing it, in prep for draw new line...
  //(It may be worth, for speed's sake, eventually redoing as...
  //   GreenLine(0,10,60,150,iPrevY[1]);, skipping the
  //         FillCoords/ 2x access to iGreenLineEnds(record)
  //But wait to see final form with all coords flexible before
  //   doing that.

  FillCoords(1,wGrMarLeft+bGrAxWidX+trunc(wGrGAWidth*10/100),
     iPrevY[2],wGrMarLeft+bGrAxWidX+trunc(wGrGAWidth*90/100),iPrevY[3]);
                                            //sbLowerLeftScroll continued....

  //Overdraw old, erasing it...
      GreenLine(
         iGreenLineEnds[1].x1,iPrevY[2],
         iGreenLineEnds[1].x2,iPrevY[3]);

  //Draw new...
      GreenLine(
         iGreenLineEnds[1].x1,wTmpYL,
         iGreenLineEnds[1].x2,iPrevY[3]);

  //Record where end of new was, for next erase...
      iPrevY[2]:=wTmpYL;

end;//sbLowerLeftScroll

procedure Tld010f1.FormClose(Sender: TObject; var CloseAction: TCloseAction);
var dfIniFile:TInifile;
begin

 SaveStockSpecificIniFile;//Note: This is not the only place this SR is called from

 dfIniFile:=TIniFile.Create(sIniFileMainName);
 with dfIniFile do begin

  //Save posn and size of application's window...
  WriteInteger('WindowPosnNSize','Top',ld010f1.top);
  WriteInteger('WindowPosnNSize','Left',ld010f1.left);
  WriteInteger('WindowPosnNSize','Height',ld010f1.height);
  WriteInteger('WindowPosnNSize','Width',ld010f1.width);

  WriteInteger('ImagePosnNSize','Left',iImageLeft);
  WriteInteger('ImagePosnNSize','Top',iImageTop);
  WriteInteger('ImagePosnNSize','Width',iImageWidth);
  WriteInteger('ImagePosnNSize','Height',iImageHeight);

  WriteString('DataLocation','Path To Price Files',sDatePriceFilesPath);
  //In the next, we strip off the .txt which is on sDatePriceFilename
  WriteString('DataLocation','Initial Stock To Display',
        copy(sDatePriceFilename,1,length(sDatePriceFilename)-4));

  //At 23 May 14, 09:27, not actually needed, as there is no code
  //  in the program to change sStockInifilesPath... but there may
  //  be one day, so...
  WriteString('DataLocation','StockIniFilesPath',sStockInifilesPath);

  end;//with...

 dfIniFile.Free;
  //showmessage('Bye');
end;

function Tld010f1.sStockSpecificIniPathAndFileName(sTmpL:string):string;
begin
 result:=sStockInifilesPath+sTmpL+'.txt';
end;

procedure Tld010f1.SaveStockSpecificIniFile;
    var dfIniFile:TInifile;
    begin

    dfIniFile:=TIniFile.Create(sStockSpecificIniPathAndFileName(sDataSet+'ini'));
     with dfIniFile do begin
       writeinteger('GreenLine','UpperLeft',iPrevY[0]);
       writeinteger('GreenLine','UpperRight',iPrevY[1]);
       writeinteger('GreenLine','LowerLeft',iPrevY[2]);
       writeinteger('GreenLine','LowerRight',iPrevY[3]);
     end;//with...
     dfIniFile.Free;

end; // of SaveStockSpecificIniFile

end.




Search across all my sites with the Google search...

Custom Search
            powered by FreeFind
  Site search Web search
Site Map    What's New    Search This search merely looks for the words you enter. It won't answer "Where can I download InpOut32?"
Ad from page's editor: Yes.. I do enjoy compiling these things for you. I hope they are helpful. However... this doesn't pay my bills!!! Sheepdog Software (tm) is supposed to help do that, so if you found this stuff useful, (and you run a Windows or MS-DOS PC) please visit my freeware and shareware page, download something, and circulate it for me? Links on your page to this page would also be appreciated!
Click here to visit editor's freeware, shareware page.

Link to Lazarus Tutorials main page
How to contact the editor of this page, Tom Boyd


Please consider contributing to the author of this site... and if you don't want to do that, at least check out his introduction to the new micro-donations system Flattr.htm....



Valid HTML 4.01 Transitional Page tested for compliance with INDUSTRY (not MS-only) standards, using the free, publicly accessible validator at validator.w3.org. Mostly passes. There were two "unknown attributes" in Google+ button code. Sigh.


If this page causes a script to run, why? Because of things like Google panels, and the code for the search button. Why do I mention scripts? Be sure you know all you need to about spyware.

....... P a g e . . . E n d s .....