This book is for anyone who wants to learn to write applications and has had no experience in it before. The programming language is Gambas and it runs in the Linux operating system. Gambas can be easily downloaded and installed from software repositories and is free in the best Linux tradition. The examples and screen snapshots in this book were made with Gambas 3.13.0. It has been a project for the first half of 2019.
Having recently completed it in LibreOffice, here it is as a wikibook. The PDF version, generated by LibreOffice, can be found by clicking the image on the right.
There are many fine applications out there. LibreOffice lets you type, format and print documents, create slide shows and calculate spreadsheets. FireFox lets you browse web pages. There are applications to send and receive emails. Still, there is nothing like teaching a computer to do something you want it to do and designing how it will look yourself. There may be things your school or work might want to do that no one has thought of—very specific things, sometimes very simple things.
You can be creative. I have a notebook application. It sits as an icon in the notifications area of the taskbar, always there. Any text I have copied to the clipboard—one click on the icon and it is saved. Right-click it and a window appears where I can search for notes just by typing. Control-click it and I can save the text on the clipboard with a key word or words attached to help me easily find that note again. Text copied from web pages often comes in separated lines, so I have a menu item to "Fix broken sentences". No formatting; no links; no pictures—just text saved with a click. With programs you write yourself, when you think of something you would like to do, you can just add it, like adding extensions to a house.
Gambas is a language and programming environment to get excited about. If you need a button, drag one onto the window. To tell it what to do when you click it, double-click the button and type the instructions. If you want a new window, choose New Window from a menu. It is as easy or as sophisticated as you want it to be. Everything your computer can do can be done through Gambas (as far as I can tell!).
The book starts with what a computer language looks like. Ordinary speech has to become computer-speak. Tell me what four times three is has to become TextBox1.text = 4*3 . TextBox1.text.font is a little like John.glasses.frame.
From examples, we discover the three things computers can do: memory, repetition and conditional execution. Memory includes remembering text and numbers, in individual memories and lists of them (arrays), and calculating with them. Repetition is doing the same thing over and over without getting bored or needing coffee breaks. Conditional execution is doing one thing or doing another depending.
Arranging buttons and boxes and things in the window is next, so things expand and contract properly when the window size is changed.
Menus and contextual menus follow. Have you ever wanted to call a menu after yourself? No, of course not.
Gambas can save settings automatically, so the application starts up the way you left it last time.
Programs need to be ordered and compartmentalised like rooms in a house. Languages have modules and classes to do that, so all the programming is not in one amorphous mass. Modules and classes are to programs what boxes and cupboards and shelves are to a house. Some can be copied and some cannot.
There is a language within a language, and that is SQL that is used to talk to database files. SQLite is introduced. I am no expert, you understand, but know enough to get by and enough to introduce the topic with an example of a program for allocating cash spending to different categories, saving it in an SQLite database.
Lastly there is how to print. This involves putting text and pictures on a page, drawing lines and boxes.
There is the game of HiLo ... the computer picks a number and you try to guess it in as few goes as possible, each time being told if your guess is too high or too low.
Another guessing game is Moo, also called Bulls and Cows. You must guess a four digit number, all digits different, being told after each guess how many digits were in the secret number in their correct places (bulls) and how many were in the secret number but were not in their right places (cows). It was marketed under the name of Mastermind.
The game of Animal has been around since the 1970s. The computer starts knowing only two animals. You teach it new animals and the right questions to ask to identify your animals. It teaches binary keys—useful in biology. It is a little artificial intelligence.
The game of Concentration, or Memory, is where you turn over cards, hoping to find a matching pair. Your task is to find all the matching pairs. We start with cards that have letters on them and adjust it so pictures can show. You could have pictures of family members if you like, because the pictures are read in from disk when the application starts.
To illustrate databases there is a Cash Spending program that allocates cash amounts to various categories and totals the spending in each category, showing what percentage of the cash was spent in each area.
There are two printing exercises: printing a class list with vertical and horizontal lines that form boxes, and printing a calendar page on A4 paper for the current calendar month. You supply the large picture for the top, and the squares with the dates in them are big enough to write in and the page can be blu-takked to a refrigerator door.
The Tray Item Notebook is a small icon for the system tray. Click on it and any text you have on the clipboard is saved as a text-only note in an SQLite database. To search your notes, middle-click (left and right mouse buttons together) the icon and a window appears. A few adjustments to the text can be made: fixing broken sentences, double-spacing the paragraphs and trimming and tidying.
Programming is making applications. It is also called ‘coding’ because the instructions are written in ‘code’.
There are many programming languages. Ours is GAMBAS. (Gambas Almost Means BASic). BASIC (Beginner's All-purpose Symbolic Instruction Code) first appeared on 1 May 1964 (55 years ago in 2019, as I write). Everyone loved BASIC. It made programming possible for us ordinary mugs. Benoît Minisini designed and wrote GAMBAS and gives it away free. He lives in Paris, France and is 47 (in 2019). He said, “Programming is one of my passions since I was twelve” and “I am using many other languages, but I never forgot that I have learned and done a lot with BASIC.”
Benoît Minisini — One really great guy—we stand in awe.
The language is Gambas. This is a Sub (also called Method, Procedure or Subroutine. If it gave you some sort of answer or result it could be called a Function).
Form with gridview
On the left is the design. It is a form. Think of it as a Window. On the right is the application in action. That is what you see when you run the program.
Starting an application is called running it. The program runs. Every statement in the program is executed. The Form_open sub (above) fills the gridview object.
Double-click FMain in the list on the left. The form opens.
Look for a GridView among the objects at the bottom right. Drag it onto the form. Adjust its size by dragging the handles (small squares on its edge). Adjust the form size too.
Right-click the form (NOT the gridview) > Event menu item > Open.
Type or paste in the code for the open event (as above).
This is the most important idea about programming. The only way to write a good program is to “divide and conquer”. Start with the Big Idea, then divide it into parts. Piece by piece, write the program. This is called top-down development. The opposite is useful, too: bottom-up development. Write a simple program and keep adding bits and pieces to it until it does all the things you want it to.
The first programs were simple creatures that did but one thing. Then came menus, where you could select one of several things by pressing a key. For example, “Press 1 to get your account balance. Press 2 to recharge. Press 0 to speak to an operator.” Nowadays applications do many things. To choose, you have a menu bar at the top, menus that drop down, and menu items that you click on in each menu. You can click a button. Buttons can be solitary, or friendly with each other and gather together in a toolbar. You can pop up a menu anywhere. You can type shortcut keys to get things to happen. There can be several windows. Within windows (forms) there can be many pages with or without an index tab at the top of each.
Buttons can be on the screen, or the physical keys on your keyboard. The standard keyboard has 101 keys, but they can all be given a second function if you hold down CTRL-, the control key, while you type them. That gives you 202. Not enough? Holding down SHIFT-, the shift key, makes all the keys different again, giving you 303 buttons. There is ALT- (alternative) that makes all the buttons different yet again: 404 of them. The modifier keys held down in combination give you more sets of keys, like more keyboards: SHIFT+CTRL, CTRL+ALT and SHIFT-CTRL-ALT. Now I have lost count. Fortunately for us, no application uses them all.
The key to good programming is to get things into order. Be neat and tidy. Arrange things. Things you do all the time should have their buttons visible; things you do not so often could be hidden away in a popup menu.
Reduce complicated tasks to a series of simple steps. Write lots of little subs, not just one big sub. Write “master subs” that call on lesser subs to do little jobs. The biggest master of them all is the user. You use the program to do this, then that, then something else. The program says, “At your service!” and calls on different subs you have written to do what you want. In the meantime, it waits and listens for your command, checking the keyboard and the mouse, or, if it is not waiting for you, it keeps busy doing something you set it to work on.
The program above has two things (called Objects): a form, and a gridview.
Things that happen are called Events.
There is an event that “fires” or “is triggered” when a form opens. It is called (no surprise) the OPEN event. If you want something to happen when the form opens, write a sub called Form_Open. The instructions there will be carried out when the OPEN event fires. The form opens when the application starts up.
You have to think of two things: what will happen and when it will happen. The “what” is the list of instructions in Subs. The “when” is a case of choosing which events to handle.
Everything has to have a name. People have names. Forms have names. Buttons have names. You get to choose, using letters and numbers, always starting with a letter, and not having any spaces.
The main form is called FMain. It's open event is Form_Open.
I like the convention of calling it FMain. The “F” says, “Form”. If I had a form that listed people's addresses, I would call it FAddress. When it opens—perhaps at the click of a button—its open event would look for a sub called FAddress_Open.
Right-click an object, choose EVENT, and click on the event you want to write some programming for:
Right-click the form > Event > Open > write code for the Open event.
There will be a big confusing list of events. Don't be fazed: only a few are used often. A big part of learning the language is getting used to the events that are most useful to certain objects. Buttons, for example, like to be Clicked; you don't use the other events very often. Possibly, entering a button might put some help text in a line at the bottom of the window, and leaving it will clear the message, but not often. I have not seen a double-click handler for a button yet: who double-clicks buttons? You will get used to the favourite events that objects have.
In the program you have written, type or paste this sub. Every time you roll the mouse into the gridview, a message saying “Hello” appears. When you have tried it and it has annoyed you enough, delete the three lines of code.
PublicSubGridview1_Enter()Message("You have entered the gridview ... evil laugh...")End
The Activate event happens when you double-click. Enter these lines and double-click any of the lines.
gridview1.row is the row you double-clicked on. It is set when you click or double-click. The “&” sign (ampersand) joins two strings of text. (Strings are text ... characters one after the other.)
This next code will do something when you click on gridview1:
PublicSubgridview1_Click()Label1.text="You clicked on row "&gridview1.Row&", "&gridview1[gridview1.row,0].textEnd
There are two strings. (1) "You clicked on row " and (2) ", " . Strings have double quotes around them.
What is on the right of the equals goes into what is on the left.
You don't put something into Label1; you put it into the text of Label1.
Usually languages won't allow this: Label1.text = 345. It must be Label1.text = "345" or Label1.text=Str(345), but Gambas doesn't mind. Gridview1.Row refers to the row number you clicked on. Numbers normally go into things that store numbers, and strings go into things that store strings, but Gambas converts it automatically.
gridview1[gridview1.row, 0].text looks complicated, but let's break it down. From the left, it is something to do with gridview1. From the right, it is the text in something. The part in square brackets is two numbers with a comma in between: the row you clicked on and zero. In the example above, it is [2,0]. Row two, column zero.
Gridview rows and Gridview columns start their numbering at zero.
In this part the examples are not Gambas, but they are written in Gambas style.
Behold John and Joan:
Parts are referred to like John.arm, Joan.head
Sometimes parts have smaller parts, such as John.hand.finger
Parts have properties, like Joan.height
Some properties are integers (whole numbers), like Joan.Toes.Count
Some properties are floats (numbers with decimal fractions) like John.height . This is not the same as John.Leg.Height .
Some properties are booleans (true/false, yes/no) like Hat On or Hat Off. We could say Persons, as a class, have a property called HatOn, “John.HatOn” or “Joan.HatOn”. (It is true or false.)
They belong to the class called “person”. John is a person; Joan is a person. Persons all have a height, weight, hat-on/hat-off, and other properties.
Persons also have various abilities. The “person” class can sing and wave and smile. Their legs can walk. For example, John.sing(“Baby Face”) . Sometimes John will need to know which version, for example, John.sing(“Baby Face”, “Louis Armstrong”) or Joan.wave(“Hello”, “John”, 8). This means “Joan! Give a hello wave to John, vigorously!”
There will also be events they can respond to, like a push, or when they hear someone singing. These events are John_push() and Joan_push(), John_HearsSinging() and Joan_HearsSinging()
Let's write a response to these events in the style of Gambas.
John might get annoyed when the Push event takes place:
Boolean properties are yes/no, true/false, haton/hatoff things. Putting false into the HatOn property means taking her hat off before she sings.
John has the ability to turn around, but we must say how much to turn. He turns 180°, doing an about-face so he is facing the person who pushed him.
Mary can sing, but we must say which song to sing. It is the same one that she is hearing. We say that the HearsSinging event is passed the name of the song, “Ave Maria”. That song title is passed to Joan's singing ability, which is referred to as Joan.sing . The bits of information you pass on so that the action can be done are called parameters. Some methods require more than one parameter, and they can be numbers (integers, floats...) or strings (text like “Jingle Bells” or “Fred” or “Quit pushing, will you?!”) or booleans (HatOn/HatOff, Complain/Don’tComplain True/False kinds of things), or other objects or, well, anything.
Let's define a sub and this time we'll put some repetition in it. There are two kinds of repetition—one where you repeat a definite number of times, and another where you have to check something to see if it is time to stop. (And the latter comes in two kinds, where you check if it is time to stop before you start or after you have done it—pretested loops or post-tested loops.) Here are the definite and indefinite types:
In the first, there will be 5 hops and that is all. “i” is an integer that is created in the line For i as integer = 1 to 5 and it counts from 1 to 5. It is made one bigger by the word NEXT. The repeated section is called a loop.
Tired() is something that has to be worked out. The procedure for working it out will be in another sub somewhere. At the end of it there will be an answer: are you tired or not? Yes or no, true or false. That will be returned as a final value. Tired() looks like a single thing, the value returned by a function. Functions work something out and give you an answer.
“Hop” might need further explanation if “hop” is not already known. You can use any made-up words you like, provided you explain them in terms that are built-in and already known to Gambas. Here is a sub that explains the Hop procedure:
PublicSubHop()GoUp()GoDown()End
The beauty of breaking procedures down into simpler procedures is that the program explains itself. You are giving names to the complicated tasks without getting lost in the fine detail of how they are done. Not only can you use words that make sense to someone else reading your program but you can track down errors more easily. You can test each part more easily. You can also modify your program more easily. DoExercises() can be left as it is, but you can change the definition of hopping with
Anything after a single apostrophe is a note to yourself. Gambas disregards it. Put in comments to remind yourself of what you are doing. It can also explain your thinking to someone else who might need to read your program. Here is some more pseudocode:
PublicSubJoan_HearsSinging(“AveMaria”)'you must say what the song isJoan.HatOn=False'remove your hat'now the fun startsJoan.sing(“AveMaria”)'sing the same songEnd
That ends the pseudocode. The next section is back into Gambas.
The code in this section is Gambas. The last section was pseudocode.
There are three things computers can do: memory, repetition and conditional execution. Repetition is doing things over and over like working through millions of numbers or thousands of records. Conditional execution means making choices.
Calculators sometimes have M+ and MR keys. Whatever number is showing goes into a memory when you press M+. Whenever you need to use that number again, to save typing it, press the Memory Recall button, MR. The memory is like a note you have made to yourself to remember this number.
Computers have as many memories as you like and you give them names. Putting something in a memory is as easy as typing Age = 23. What is on the right is put into the memory on the left. To see what is in the memory called Age, print it or put it in some place where you can see it. Label1.text = "Your age is " & Age .
Before you can use some name as a memory you must tell Gambas what kind of thing will be stored in it. This is what the DIM statement does. You declare it before it is used. For example, Dim Age As Integer or Dim FirstName as String . Memories are called Variables or Properties if they are associated with something.
You can declare a memory and put something into it in one line:
DimFirstNameasString="Michael"
You can use memories to calculate something, and put the answer into another memory:
There are six labels, three textboxes and one button. The names for the labels do not matter, but the textboxes and the button are named as shown. The program is:
“Val” takes a string and converts it to a number. It means “the value of”.
It could be written this way, using variables, but it would take more lines:
And you could write a function that takes distance and time and returns the speed. It is good to teach the computer things. Here we have taught the computer that Speed(5,2) is 2.5. Our calculate button uses it. We could have a menu item that also uses it.
Now let’s trick our program. Don’t put in anything for the distance or time. Just click the Calculate button. The poor thing cannot cope. We get this error:
Problems come at the extremes. In repeated sections, they are most likely to occur in the first or the last repetition. Here, there is an extreme input: nothing, zip, nilch. Gambas cannot get the Val( ) of that.
tbKilometres.text was Null. We should anticipate that someone might click the button without putting in any numbers. Here are two ways handle the situation, and the second one is better because ‘prevention is better than cure’:
1. Finish early (Return from the sub early)
PublicSubbCalculate_Click()Dimd,t,sAsFloatIfIsNull(Val(tbKilometres.text))ThenMessage("Hey, you there! Give me a NUMBER for the kilometres.")ReturnElsed=Val(tbKilometres.text)EndifIfIsNull(Val(tbHours.text))ThenMessage("A number would be nice for the hours. Please try again.")ReturnElset=Val(tbHours.text)Endifs=d/ttbSpeed.text=s'To round to 2 decimal places, change s to format(s,"#.00")End
2. Only enable the button if the calculation can proceed. Set the Enabled property of the button to False to begin with. We need to handle a few events.
PublicSubbCalculate_Click()tbSpeed.text=Val(tbKilometres.text)/Val(tbHours.text)Fmain.SetFocus'otherwise the button stays highlighted; focus the formEndPublicSubtbKilometres_Change()CheckBothAreNumbersEndPublicSubtbHours_Change()CheckBothAreNumbersEndPublicSubCheckBothAreNumbers()bCalculate.Enabled=NotIsNull(Val(tbKilometres.text))AndNotIsNull(Val(tbHours.text))End
bCalculate.Enabled = means ‘set the enabled property of the button to...’
Not IsNull(Val(tbKilometres.text)) means Yes if the text in the kilometres textbox can be converted to a number.
Every textbox has a Change event. It fires when the text in the box changes. Every time you press a key, that Change event is going to see if it’s time to enable the bCalculate button.
And because I cannot leave well enough alone, I apologise for introducing the “Group” property. You may need coffee. Or skip this section. Do you notice that the two textboxes have to each handle the Change event the same way? tbKilometres and tbHours both check for numbers and enable or disable the button accordingly. Wouldn’t it be nice if both textboxes were like just one textbox? Gambas can do this. Put them in a Group. Then this single group will have a Change event, and you can handle it just once. Group is a property. Find the Group property for each and set it to “InputBox”. Now your code becomes the simplest yet:
PublicSubbCalculate_Click()tbSpeed.text=Val(tbKilometres.text)/Val(tbHours.text)Fmain.SetFocus'otherwise the button stays highlighted; focus the formEndPublicSubInputBox_Change()CheckBothAreNumbersEndPublicSubCheckBothAreNumbers()bCalculate.Enabled=NotIsNull(Val(tbKilometres.text))AndNotIsNull(Val(tbHours.text))End
It is as if the two textboxes have become inputboxes (a name you just invented), with all the same events. One set of event handlers for several objects.
It’s robust (resistant to users who insist on not using your application the way you expect them to). It’s concise (3 event handlers, 4 lines of code). It works. One thing remains: a button that says QUIT with one thing in its Click event: the command Quit. Over to you.
This game is easy to play: one person thinks of a number between 1 and 100. You try to work out the number in as few guesses as possible. Each time you will be told “Too High!” or “Too Low!”.
There are three labels and one textbox. The large label where it says “Guess...” is named labMyReply. The textbox is named tbGuess. The small label in the bottom left corner is named labCount.
PublicMyNumberAsIntegerPublicSubForm_Open()MyNumber=Rand(1,100)EndPublicSubtbGuess_KeyPress()IfKey.Code=Key.EnterOrKey.Code=Key.ReturnThenGiveMessageEndPublicSubGiveMessage()IfIsNull(tbGuess.text)Then'no guess at alllabMyReply.text="Guess..."ReturnEndifDimYourGuessAsInteger=Val(tbGuess.text)IfMyNumber=YourGuessThenlabMyReply.Text="Right! "&MyNumberMyNumber=Rand(1,100)tbGuess.text=""FMain.Background=Color.GreenlabCount.text="0"ReturnEndifIfYourGuess>MyNumberThenlabMyReply.Text="Too High!"ElselabMyReply.Text="Too Low!"tbGuess.text=""FMain.Background=Color.PinklabCount.text=Val(labCount.text)+1End
The program checks the guess when ENTER or RETURN is pressed in the textbox. The Key class is static, meaning it is always there—you do not have to declare or create it. If you ever need to check for some key, such as SHIFT-C, being pressed, you would write:
The Stop Event line is there to prevent capital-C being printed in the textbox, which would normally happen when you type Shift-C in a textbox.
Public MyNumber As Integer means we want a public property called MyNumber. You could say Private instead of public. Private properties are accessible only in the code belonging to that form. If we had other forms (i.e. windows) they cannot see another form’s private property. Declaring a property as private is a way of telling you, the programmer, that you have only used this property here, in this form’s code that you are looking at. Also, another form could have its own property and use the same name.
In the Event Form_Open() event handler, Rand(1,100) is a built-in function that represents a random number between 1 and 100.
In the Event tbGuess_KeyPress() event handler, the key just typed is compared with the Enter and Return keys. If it was either one, the guess is checked. Every key has a number. Enter is 16777221 and Return is 16777220. We do not need to know the numbers, because they are stored in Key.Enter and Key.Return. These are constants in the Key class. Because the Key class is static we do not have to make a new example of it: it is always there and we can refer to it by its name. We never need another one of them. There is only ever one keyboard. We never have to say “the Key belonging to the ACER laptop keyboard” or “the Key that was typed on the HP laptop keyboard”.
If IsNull(tbGuess.text) Then … End avoids the nasty situation of a person pressing Enter without having typed in any number at all.
Dim YourGuess As Integer = Val(tbGuess.text) puts the numeric value of what was typed into a variable called YourGuess. This is an integer. If a person typed 34.56, only 34 would be put into YourGuess.
If MyNumber = YourGuess Then… checks to see if YourGuess matches MyNumber. If it is, show the “Right!” message and make the background colour of the form green. Choose another random number ready for the next game and then Return, because nothing more needs to be done.
If YourGuess > MyNumber Then labMyReply.Text = "Too High!" Else labMyReply.Text = "Too Low!" puts the appropriate message into the labMyReply textbox. This is an If...Then...Else statement all on one line. In either case, continue on to make the background pink.
From the Gambas help page, these are the properties and constants for the Key class:
Screenshot of Links on a Gambas help page
The constants return integer numbers. The properties are what was typed. So you could check if the user typed the PgDown key with if Key.Code = Key.PgDown then… Negotiating and reading the help pages is a skill in itself.
Let’s type numbers into a TableView and make it work out whether each student earns an A, B, C, D, E or F grade. We’ll need two columns. The first will be for the marks, the second for the grades.
We’ll skip having a column for student names for now. And we’ll avoid using many IF...THEN... statements in favour of its big brother, SELECT...CASE…
The TableView is named tvMarks. The textbox is named tbRows. Double-click an empty part of the form and enter the code for the form’s open event:
The tvMarks_Click() handler lets you type in the cell if the column is 0. If you click in column 1 you will not be able to type: nothing happens when you click.
You might think that whatever you type in a cell should show up. It doesn’t. It raises the Save event. You might want something else to appear other than what was typed. During the Save event, actually put the text that was typed into the text property of the cell:
tvMarks[Row, Column].Text = Value
The Save event comes with three parameters that you can use freely in the course of the event handler: tvMarks_Save(Row As Integer, Column As Integer, Value As String) . This line of code puts the value that was typed into the text of the cell. Which cell? tvMarks[row, column]. That is the cell.
You refer to a cell by using square brackets: tvMarks[1,0] refers to row 1, column 0. (Remember rows and columns are numbered starting with zero.) tvMarks.Rows[2] is row 2. tvMarks.Columns[1] is the whole of column 1.
The TableView_Data() event is very useful. It is raised (happens) every time a cell needs to be redrawn on the screen. (It is useful to remember it deals with cells, not rows or columns or the whole table.) Right-click the tableView, then > Event > Data and enter this:
This gives alternate rows a light blue colour (very pretty). To explain, cells have a property called “background”. It is a colour. Colours can be described in several ways: using a straight number is the simplest. The number is &F0F0FF.
A computer screen is full of little lights that light up Red, Green and Blue.
What sort of number is &F0F0FF? The “&” sign means the number is written in hexadecimal. Normally we use the decimal system, which is Base 10. You count from zero to nine and the digit goes back to zero and you increase the digit to its left by one. Here is normal counting in Base 10: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,16… Using hexadecimal you have 16 digits, not ten. Here is counting in Base 16: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 1A, 1B, 1C, 1D, 1E, 1F, 20, 21… The eleventh number in the decimal system is written 11. The eleventh number in the hexadecimal system is written B.
Now, what colour is &F0F0FF? The hexadecimal number F0F0FF is actually three 2-digit numbers: F0, F0, FF. The first number is how much Red. The second number is how much Green. The third number is how much Blue. That is RGB, red-green-blue. Each goes from 00 to FF. 00 in the first place would mean “No red at all”. FF in the first place would mean “Maximum red!”. Pure red would be &FF0000. Pure green would be &00FF00, and pure blue is &0000FF. So pure black is &000000. Pure white is &FFFFFF, which is all the colours as bright as you can make them.
This colour is grey: &F0F0F0. The red, green and blue lights are mostly on, but not fully bright. F0 is not as bright as FF. To get a darker grey, lower the numbers but lower them all the same, e.g., C0C0C0. Darker again, B0B0B0. The tiniest bit darker than that is AFAFAF, because after AF comes B0. The point is, when they are all the same you get shades of grey. So look at F0F0FF. It is a very light grey (F0F0F0), but the last number, the one for Blue, is a bit brighter. It is FF. So the colour is a very light grey with the Blue just a bit brighter. That is, it’s very light blue. It is all about the mix of three colours, red, green and blue. This is pale pink: FFD0D0. This is pale green: D0FFD0. This is pale yellow: FFFFD0. (Red=brightest, Green=brightest, Blue=a little less bright). This is full yellow: FFFF00.
It is all about the tiny little LED lights on your screen. There are millions of them, but they are grouped in threes—a red, a green and a blue. You are control the brightness of each little light. The brightness goes from 00 to FF, or in decimal, from 0 to 255. (FF=255). One red, one green and one blue act like one coloured dot. It is called a pixel. A typical laptop screen has a resolution of 1366 x 768 pixels. That is 1,049,088 pixels. Each has three little LED lights, making 3,147,264 lights on your screen, each one with 256 shades of brightness. Amazing!
Click “+Insert”. Name this menu MenuFile and caption it File. The caption is what the user sees. The name is how we refer to the menu in our programming.
Click “+Insert” again. We want it to be part of the File menu, so click the push-right button on the toolbar. While you are at it, give the menu CTRL-Q for a shortcut.
Adding a Quit menu itemAdding Ctrl-Q as a keyboard shortcut
Now write a handler for when the user clicks on MenuQuit. (To do this, look for the File menu in the form and click it, then click the menuitem Quit.)
i is an integer that counts 1, 2, 3, 4, 5. Don’t put “as integer” if you already have DIM i As Integer
You can use i in the loop, but do not change its value. The word NEXT increases it by one and sends the computer back to the start (the FOR line) where i is checked to see if it is bigger than 5. If it is, it goes to the line following NEXT.
PublicSubButton1_Click()DoRepeat'move to the rightButton1.x+=1Wait0.001UntilButton1.x+Button1.w=FMain.WidthRepeat'move to the leftButton1.x-=1Wait0.001UntilButton1.x=0LoopEndPublicSubForm_KeyPress()QuitEnd
The program ends when you press a key. Wait 0.001 delays progress for one-thousandth of a second. The delay allows the button to be redrawn. X and Y are traditionally used for Across and Down. The button moves from left to right and back, so it is Button1’s X that we need to change. The button keeps moving to the right until its left side plus its width is equal to the width of the form. In other words, it stops moving right when the right side of the button meets the right edge of the form. After that we start subtracting from X until it meets the left edge of the form. Try changing the size of the window while the button is in motion: the button still moves to the edge.
Notice how the totals cell is blue on light grey, and the text is bold. "<b>" & t & "</b>" are tags each side of the total. Rich text is text with tags in it. The first tag is “switch bold on” and the second, with the slash, is “switch bold off”.
The Save event occurs when Enter or Return is pressed. After putting the typed number into the cell with tv1[Row, Column].Text = Value, the new total is put into the richtext of cell tv1[5, 0].
Gambas allows these tags in rich text. They are part of HTML, HyperText Markup Language, which web browsers use to display web pages. Each is switched on first, then switched off at the end, e.g. "<b><i>This is Bold Italic</b></i>".
<h1>,<h2>, <h3>, <h4>, <h5>, <h6> →Headlines
<sup> Superscript
<b> Bold font
<small> Small
<i> Italic
<p> Paragraph
<s> Crossed out
<br> Line break
<u> Underlined
<a> Link
<sub> Subscript
<font> Font
The Font tag is used this way: "<Font Color=#B22222>" & "<Font Face=Candara>" & t & "</Font>"
The Paragraph tag denotes paragraphs, which are usually separated by a blank line by browsers. It can used this way: <p align=right>Some Text</p> but on one web page, referring to HTML, it said, “The align attribute on <p> tags is obsolete and shouldn't be used”.(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/p). In the meantime, it works.
<tt> single-line source text
<pre> preformatted text (preserves spacing)
<hr> horizontal line (no end tag needed)
<ul> unsorted list
<li> list
<ol> list
<ul><li>Jim</li><li>John</li><li>Ann</li></ul> will give bulletted points in a vertical list.
<ol><li>Jim</li><li>John</li><li>Ann</li></ol> will give numbered points in a vertical list.
There are times when you need not just separated memories with individual names like Speed, Distance, Time but a list of memories, a collection of variables, that can be numbered. So you might have Speed[0], Speed[1], Speed[2] and so on, all storing different speeds, or a list of several times that have names like Time[0], Time[1], Time[2] etc. This is called an array.
The elements (items) in the array are numbered, starting from zero, and you use square brackets. Teachers might have an array of student names, Student[0], Student[1] … Student[Student.Max]. Arrays have a count (e.g. Student.Count) and a max (Student.Max). Don’t go past Max or you will be out of bounds. And that means detention after school for sure.
You can have arrays of just about anything. However, you need to create them with the NEW operator when you want them.
To see your array, put a listbox on your form then put your array into its list. The array on the right goes into the listbox’s list on the left. The code will look something like this. More detailed instructions follow.
Listbox1.List=NamesListbox1.List=Numbers
The buttons are called bShuffle and bSort.
PublicsAsNewString[]PublicSubForm_Open()s=["Apple","Banana","Carrot","Dragonfruit","Elderberry","Fig","Grape","Honeydew","Imbe","Jackfruit","Kiwi","Lime"]ListBox1.List=sEndPublicSubbSort_Click()s=ListBox1.List'copy the list into array s[]ListBox1.List=s.Sort()'put the sorted s[] into the listbox's listEndPublicSubbShuffle_Click()s=ListBox1.List'copy the list into array s[]s.Shuffle'shuffle the array s[]ListBox1.List=s'put s into the listbox's listEnd
Notice that you cannot say ListBox1.List.Shuffle, even though ListBox1’s List property acts like an array. Yes, it is an array. No, it doesn’t come with the Shuffle method.
The shuffle button has an extra line in its Click handler compared with the sort button. s.Sort() is a thing, a function. s.Shuffle is a method. It is not a thing but a process, a procedure. It is a sub that does not return any value when it is done. You cannot put it into something. If you tried ListBox1.List = s.shuffle() you would get an error message. The Gambas help shows how they are different:
A listbox with numbers needs to be sorted numerically. Sorting number 1 to 12 alphabetically will give you 1, 10, 11, 12, 2, 3, 4, 5, 6, 7, 8, 9 because alphabetically “10” comes before “2”. Copy the list into an integer array and sort that:
Strings can be treated as arrays of characters. So, if Nm = "Gerard" then Nm[0] is G, Nm[1] is e, Nm[2] is r and so on.
However, you cannot change letters like Nm[2]="x" to make Gerard become Gexard. You can get the letter, but you can’t put something into it. You would have to use some of the wonderful functions that strings have. You can do many things with strings. You could use any of these:
You can have memories arranged in a list. They would all have the same name, but be numbered like X[0], X[1], X[2] etc:
You can have memories arranged in a grid (square or rectangle) and refer to them by row and column. This is also how the cells are numbered in a GridView or TableView:
You can have them arranged in a cube or prism, so that there are like layers of rectangles. The third number in brackets tells which layer. [Row, Column, Layer]
You can have arrays of them, too, but even though you can have multi-dimensional arrays, simple list arrays are the ones most often used.
This program fills a grid with calculations for the numbers one to ten. It also shows how to adjust properties of a gridview—its columns, rows and cells.
Things to notice are the With … End With lines, setting up the gridview when the form opens, and the _Data() event to fill each cell. gv1.Rows.Count = 10 is enough to trigger the Data() event for every cell in those ten rows.
The _Data() event occurs when the cell has to be painted. The filling of the cells could be done when the Fill button is clicked, but then we would have to use nested For statements to count down the rows and across the columns. Gambas already has to do this, because it has to paint each cell. The _Data() event happens for each cell in each row, so why not put the text into the cells then?
If Row Mod 2 = 0 Then gv1.Data.Background = &HBFFFBF sets the background of the cell to peppermint green if the row number has a remainder of zero when divided by 2. Mod means “the remainder when you divide by ...”. For example 3 Mod 2 is 1, so row 3 has a white background. 4 Mod 2 is 0 because four divided by two equals two remainder zero, so row 4 has a green background.
We could alternate two colours, pink and green, with
gv1.Data.Background = If(Row Mod 2 = 0, &HBFFFBF, &HFFCFCF)
To colour the columns, try this in the tv1_Data() event:
Format(Sqr(x), "##.000") is an interesting expression. The Format function takes a floating point number like 1.41421356237 and formats it according to the pattern supplied. "##.000" means two digits if you need them, a dot, and three decimal places using zeros. What number to format? Sqr(x). This is the square root of the number x. The square root of two appears as 1.414.
To play the game of Moo, also called Bulls and Cows, one person (the computer!) chooses a mystery number. It has 4 digits, and all digits are different. Wikipedia says, “The modern game with pegs was invented in 1970 by Mordecai Meirowitz, an Israeli postmaster and telecommunications expert. It resembles an earlier pencil and paper game called Bulls and Cows that may date back a century or more.” “Moo was written in 1970 by J. M. Grochow at MIT in the PL/I computer language.”
After each guess the computer tells you how many bulls you scored and how many cows. A bull is a digit in your number that is in its correct place in the mystery number. A cow is a digit that is present in the mystery number but is in its wrong place. Thus you are aiming for BBBB, all four digits in their correct places. CCCC means you have the correct digits but they are not in their right places. BBC means two digits are correctly placed, one of the other digits is in the number but in the wrong place, and another digit is not in the number at all. Some people play by the rule that you win if you guess it in ten or fewer turns. Bulls are listed first and cows second. You are not told which digits are the bulls or which are cows.
You need 2 textboxes, a gridview called gvHistory, and a button called bNewGame with the text property “New Game”.
The New Game button is initially invisible.
PublicMyNumberAsStringPublicCountAsIntegerPublicSubForm_Open()gvHistory.Columns.Count=2ChooseNumberEndPublicSubChooseNumber()DimDigitsAsNewString[]Dimi,p1,p2AsIntegerDimdAsStringFori=0To9Digits.Add(Str(i))NextDigits.ShuffleMyNumber=Digits[0]&Digits[1]&Digits[2]&Digits[3]EndPublicSubEvaluateGuess()DimsAsStringDimi,jAsIntegerDimYourGuessAsString=tbGuess.textCount+=1Fori=0To3'look for bullsIfYourGuess[i]=MyNumber[i]Thens&="B"NextFori=0To3'look for cowsForj=0To3Ifi<>jAndYourGuess[i]=MyNumber[j]Thens&="C"NextNexttbReply.Text=sgvHistory.Rows.Count+=1gvHistory[gvHistory.Rows.max,0].text=YourGuessgvHistory[gvHistory.Rows.max,1].text=sIfs="BBBB"ThenCongratulatetbGuess.SelectAllEndPublicSubCongratulate()Message("<b>Congratulations!</b><br><br>Got it in "&count)FMain.Background=Color.Yellow'&FFFF00bNewGame.Visible=TrueEndPublicSubbNewGame_Click()FMain.Background=Color.DefaultbNewGame.Visible=FalsegvHistory.Rows.Count=0Count=0ChooseNumbertbReply.text=""tbGuess.text=""tbGuess.SetFocusEndPublicSubtbGuess_Change()IfLen(tbGuess.text)=4ThenEvaluateGuessEnd
Now for the post-mortem:
There are two public variables. MyNumber is the computer’s secret number. Count keeps count of how many guesses.
Form_Open() On startup, set gridview to 2 columns and choose the secret number.
Public Sub ChooseNumber() Put digits 0 to 9 in a 10-item array called Digits and shuffle. The secret number is the first four digits. They will be random and none is repeated.
Public Sub EvaluateGuess() When you evaluate a guess, it is one more guess so add 1 to Count. Look for Bulls: Is the first character in your guess the same as the first character in the number? Check second, third and fourth characters too. Each time two characters match, take what s was and add a B to the end of it using s &= "B".
In looking for cows, i goes through 0, 1, 2, 3, looking through your number each time. For each one of those digits in your number, check all the digits in the secret number looking for a match, but disregard where the position numbers are the same (like the third digit in the secret number and the third digit in my guess), for that is a bull and has already been found.
S is a variable that contains BBBB or BBCC or B or whatever.
Add a row to the history gridview and put the guess in the first column and its evaluation into the second column.
Public Sub Congratulate() Show a message saying “Congratulations! You got it in 8” or whatever. Change the form’s colour to yellow. Make the New Game button visible.
Public Sub bNewGame_Click() To start a new game, remove the yellow colour, hide the New Game button, remove everything from the history gridview, set the count of guesses back to zero, choose another number, blank out the two textboxes, and set the focus to the textbox where you type in a guess so you are ready to start typing.
Public Sub tbGuess_Change() When the text in the Guess textbox changes because you have typed another digit, check the length of what is typed and if it is 4 characters long, evaluate the guess.
Let us make it that typing a question mark means “I give up—tell me the answer”. We need as new event, tbGuess_KeyPress(). As soon as a question mark is typed, this handler will check the static class, Key, to see if the character typed was “?”. If so, message us the correct answer and start a new game.
Starting a new game is exactly what we have in the _Click handler for the New Game button. This code needs to be taken out of the _Click handler and put in a sub of its own. We can call on this NewGame sub when the button is clicked or when the user gives up by typing “?”. Here is the rearranged and extra code:
PublicSubbNewGame_Click()NewGameEndPublicSubNewGame()FMain.Background=Color.DefaultbNewGame.Visible=FalsegvHistory.Rows.Count=0Count=0ChooseNumbertbReply.text=""tbGuess.text=""tbGuess.SetFocusEndPublicSubtbGuess_KeyPress()IfKey.Text="?"ThenMessage("My number was "&MyNumber)NewGameStopEventEndifEnd
Stop Event is there to prevent the question mark appearing in the tbGuess textbox. It is not necessary.
The important principle is (and you can memorise this or take this to the bank) if you want to refer to the same lines of code in two or more places, put them in their own sub and call on them by name.
Add 2 to whatever x used to be, then put the answer back into x.
x += 2
The same thing: x becomes whatever it was (=), plus (+) 2.
x = x * 4
Multiply x by 4, and put the answer back into x.
x *= 4
The same thing: x becomes whatever it was (=), times (*) 4.
s = s & "abc"
s becomes whatever s was and (&) “abc” tacked onto the end of it.
s &= "abc"
The same thing: s becomes whatever it was (=) and (&) “abc” onto the end of it.
There are numeric operators including ^ to mean “raised to the power of”. Boolean operators are AND, OR and NOT. There are others but these are the most used and useful.
The Animal Game, in which the computer tries to guess the animal you are thinking of by asking you questions, was around well before people had their own computers. It dates to at least the early 1970s. The author of website http://www.animalgame.com/ says that he/she first saw it in "101 BASIC Computer Games" (ed. by David H. Ahl - Maynard, Mass., Digital Equipment, 1973.) I remember that book. The game was originally developed by Arthur Luehrmann at Dartmouth College. http://www.smalltime.com/Dictator has an online version where you guess the dictator instead of an animal.
The computer starts by knowing only two animals, BIRD and FISH and only one question that can tell the two apart, “Does it swim?”. Questions can be answered YES or NO. If your animal is neither of these, you are asked for a question that would identify your animal. Gradually a list of animals and questions is built up, and the computer becomes smarter and smarter. It is a simple form of Artificial Intelligence (AI). Some wit once said, “Natural stupidity beats artificial intelligence every time”.
To someone starting computing, it may be too complicated. However, there might be bits here and there—saving and loading text from a text file, or the managing of big programs by breaking them into small, meaningful subs, for example—that would be useful. Let it wash over you and get a general feel of things. Practise debugging when you have made a typing error and the program does not run. I was intrigued by it back in the ‘70’s, and it is too good not to include.
Here is a typical dialogue:
Are you thinking of an animal? Yes (No ends the program.)
It is a fish. No.
The animal you were thinking of was a …? Dog
Please type in a question that would distinguish a dog from a fish. Does it have legs?
For a dog the answer would be… Yes
Are you thinking of an animal? Yes
Can it swim? Yes
Does it have legs? Yes
It is a dog. Yes
Here is a data file. It is text only. At the end of every question is the line to go next for yes or no.
0. Can it swim?/1/2
1. Does it have legs?/3/4
2. Does it have 2 big ears?/5/6
3. dog
4. Does it blow air?/9/10
5. rabbit
6. It is little and does it bite you?/7/8
7. mosquito
8. Is it small?/11/12
9. whale
10. fish
11. fly
12. bird
The code refers to these objects by the following names. Here goes. The coloured text at the top is TextLabel1. The “Are you thinking of an animal?” line has label LabPrompt and buttons bYes and bNo. The “The animal you were thinking of was a ...” has label LabPromptForAnimal and textbox tbNewAnimal. The “Please type in a question...” line has label LabQuestionPrompt. That is followed by the long textbox tbQuestion. Under it is another label LabQuestion. The last line, “For a 1111 the answer would be...” has label LabPrompt2 and buttons bYes2 and bNo2. The bottom line of buttons are named, from the left, bShowData, bReset, bSave, bOpen and bQuit.
Next is another form that allows you to look at the data. It shows by clicking the Data… button.
The form is called FData. It will automatically adjust the positions of objects on it when it is resized by dragging its corner handles. To do this, set its Arrangement property to Vertical. This makes its objects stack from top to bottom. The textarea, taData, has its expand property set to True so that its size will expand to fill the space available.
The three buttons, bSave, bCopy and bClose, are in a red rectangle the is a HBox. HBoxes spread their contents horizontally. This one is called HBox1. There are a couple of panels called Panel1 and Panel2. One is above the hbox and one is between the Copy and Close buttons, to separate them. There is a neat little spring called Spring1 that pushes the Save button to the left and the Copy button, tiny separator panel and Close button to the right.
When the game starts, these are the only visible objects. All others have their visible property set to False. The program sets their visibility to true later. There are four areas. Depending on the stage of the game you are up to, one by one they are made visible and the others are hidden.
Look again at the sample data file:
0. Can it swim?/1/2
1. Does it have legs?/3/4
2. Does it have 2 big ears?/5/6
3. dog
4. Does it blow air?/9/10
5. rabbit
6. It is little and does it bite you?/7/8
7. mosquito
8. Is it small?/11/12
9. whale
10. fish
11. fly
12. bird
If the line counter L arrives at a line with an animal name and that name is rejected, the program replaces that line by the new question you give it and the new animal and the wrong animal are added to the end of the list.
The variables declared as Public and Private right at the start are there so that they can be accessed by several different subs. They don’t just last for the duration of the sub. They belong to the form rather than any particular sub. Z[ ] needs to be accessed by the other form, too, so it is Public.
PubliczAsNewString[]'databasePrivateRightAsInteger'line to go to if Yes is clickedPrivateWrongAsInteger'line to go to if No is clickedPrivateQuestionPromptAsStringPrivateAnimalPromptAsStringPrivateNewAnimalAsStringPrivateAskedAQuestionAsBoolean'true if we've just said, "Is it a...?"PrivateNewQuestionAsStringPrivateLAsInteger'which line in database?PrivateFromYesLinkAsBooleanPublicSubbQuit_Click()QuitEndPublicSubbShowData_Click()FData.ShowModalFMain.SetFocusEndPublicSubForm_Open()'Make sure the NoTabFocus property is set to true for all buttons, otherwise one will be highlighted when you start.QuestionPrompt="Please type in a question that would distinguish a<br><b>1111</b> from a <b>2222.</b> <br><br>(e.g. Does it have...? Can it...? Is it...? ):"AnimalPrompt="For a <b>1111</b> the answer would be..."StartGame(True)'clears data tooEndPublicSubbSave_Click()SaveDataEndPublicSubSaveData()Dialog.Filter=["*.txt","Text Files"]IfDialog.SaveFile()ThenReturnFile.Name=File.BaseName&".txt"File.Save(Dialog.Path,z.Join(Chr(13)))FMain.SetFocusCatchMessage.Info(Error.Text)EndPublicSubbOpen_Click()Dialog.Filter=["*.txt","Text Files"]IfDialog.OpenFile()ThenReturnDimsAsString=File.Load(Dialog.Path)z=Split(s,Chr(13))FMain.SetFocusCatchMessage.Info(Error.Text)EndPublicSubShowStep(StageAsInteger)'1, 2, 3 or 4labPrompt.Visible=(Stage=1)bYes.Visible=(Stage=1)bNo.Visible=(Stage=1)labPromptForAnimal.Visible=(Stage=2)tbNewAnimal.Visible=(Stage=2)labQuestionPrompt.visible=(Stage=3)tbQuestion.Visible=(Stage=3)labQuestion.Visible=(Stage=4)labPrompt2.Visible=(Stage=4)bYes2.Visible=(Stage=4)bNo2.Visible=(Stage=4)SelectCaseStageCase2tbNewAnimal.SetFocusCase3tbQuestion.SetFocusCaseElseFMain.SetFocus'a futile attempt to stop buttons being highlightedEndSelectEndPublicSubbReset_Click()StartGame(True)'clear all data tooFMain.SetFocusEndPublicSubStartGame(ClearDataTooAsBoolean)IfClearDataTooThenz.Clearz.add("Can it swim?/1/2")z.add("fish")z.Add("bird")EndifL=0Right=0Wrong=0AskedAQuestion=TruelabPrompt.text="Are you thinking of an animal?"tbQuestion.Text=""tbNewAnimal.Text=""ShowStep(1)EndPublicSubtbNewAnimal_KeyPress()'Enter should cause the LostFocus eventIfKey.Code=Key.EnterOrKey.Code=Key.ReturnThenFMain.SetFocusEndPublicSubtbNewAnimal_LostFocus()'by pressing Enter or clicking elsewhereNewAnimal=LCase(tbNewAnimal.text)IfIsNull(NewAnimal)ThenReturn'user pressed enter without typing anything; don't proceed.DimsAsString=Replace(QuestionPrompt,"1111",NewAnimal)'Please type in a question...s=Replace(s,"2222",z[L])labQuestionPrompt.text=sShowStep(3)EndPublicSubtbQuestion_KeyPress()IfKey.Code=Key.EnterOrKey.Code=Key.ReturnThenFMain.SetFocusEndPublicSubtbQuestion_LostFocus()NewQuestion=tbQuestion.TextIfIsNull(NewQuestion)ThenReturn'user pressed enter without typing anything; don't proceed.NewQuestion=UCase(NewQuestion[0])&Right(NewQuestion,-1)'capitalise first letterIfRight(NewQuestion,1)<>"?"ThenNewQuestion&="?"labQuestion.Text=NewQuestionlabPrompt2.Text=Replace(AnimalPrompt,"1111",NewAnimal)'For a gorilla the answer would be...ShowStep(4)EndPublicSubAskQuestion()DimkAsNewString[]k=Split(z[L],"/")labPrompt.text=k[0]Right=Val(k[1])Wrong=Val(k[2])AskedAQuestion=TruetbNewAnimal.SetFocusEndPublicSubMakeGuess()labPrompt.Text="It is "&If(InStr(Left(z[L],1),"aeiou")>0,"an ","a ")&z[L]&"."AskedAQuestion=FalseEndPublicSubbYes_Click()'Yes has been clickedIfAskedAQuestionThenL=Right'take the right forkIfInStr(z[L],"/")>0ThenAskQuestionElseMakeGuessElse'made a guess...StartGame(False)EndifFMain.SetFocusEndPublicSubbNo_Click()'No has been clickedIflabPrompt.text="Are you thinking of an animal?"ThenQuit'If you won't play, I quit!IfAskedAQuestionThenL=Wrong'take the left forkIfInStr(z[L],"/")>0ThenAskQuestionElseMakeGuessElse'made a wrong guess, so add an animalShowStep(2)EndifEndPublicSubbYes2_Click()DimsAsString=NewQuestion&"/"&Str(z.max+1)&"/"&Str(z.max+2)z.Add(NewAnimal)'the right animalz.Add(z[L])'the wrong animalz[L]=s'replace the earlier wrong animal with the new questionStartGame(False)EndPublicSubbNo2_Click()DimsAsString=NewQuestion&"/"&Str(z.max+1)&"/"&Str(z.max+2)z.Add(z[Wrong])'the wrong animalz.Add(NewAnimal)'the right animalz[Wrong]=s'replace the earlier wrong animal with the new questionStartGame(False)End
The code for the FData form is:
PublicSubForm_Open()taData.Text=FMain.z.Join(Chr(13))'Chr(13) is ReturnEndPublicSubbClose_Click()Me.closeEndPublicSubbCopy_Click()Clipboard.Copy(taData.text)EndPublicSubbSave_Click()FMain.SaveDataEnd
Notes:
Fmain.SetFocus
When you click a button it highlights. To make the highlight go away at the end of doing whatever it does, set the focus on the form. It seems to be needed.
Dim z as new string[]
z = split( "a/b/c" , "/" )
z will get three rows. z[0] = “a”, z[1] = “b”, z[2] = “c”
You can specify the separator, and it may be more than one character long.
Dim s as string
s= z.join(" ")
Joins the array z into one string with spaces in between
IsNull(s)
This is true if the string s is empty (no characters in it)
Chr(13)
Every character has a code number. 13 is the number for Return. If you are interested, A is chr(65) and a is char(97). The code is called ASCII.
Public Sub ShowStep(Stage As Integer)
labPrompt.Visible = (Stage = 1)
This sub needs a number when it gets called. So you might say
ShowStep(1) or ShowStep(2).
If LabPrompt.Visible is true the label will be visible. If false, it is invisible.
LabPrompt’s visibility depends on the number you supply being 1.
if InStr(z[L], "/") > 0 Then
If the string z[L] has a slash in it, then do something. InStr(LookInThis, ForThis) gives the position of the second string in the first.
Gridviews and TableViews are often made to stretch and shrink when the window they are in is resized. A form, which is a window, knows how to resize and arrange the controls that are in it. Even when the form opens any controls that can be arranged and expanded will be.
Make a small form with one TableView.
Set the arrangement property of the form to Vertical. Set the expand property of the tableview to True. Run the program (f5). Change the size of the window and notice how the size of the tableview changes with it.
Adjust the padding property of the form to 8. Run the program again (F5). The space between the tableview and the edge of the form has increased. The margin inside the form is the padding. Table cells also have padding.
Add a button under the tableview. Run the program again. Move the button to one side of the tableview. Again run the program. Change the arrangement property to Horizontal and try again. Change it back to Vertical.
Delete the button. Add a HBox. Add a button to the HBox. (Alternately, Right-click the button and choose “Embed in a container”, then right-click the container—which is a panel—and Change into... a HBox.)
Controls in a HBox are arranged horizontally.
The spring will push the button to the right as far as it will go. Experiment with and without the spring.
If you make the button wider or narrower it stays whatever width you give it in the HBox.
The button expands vertically to fill the HBox from top to bottom. Change HBox’s height and see. HBoxes expand their controls to fill their height. In panels the button stays the same height but you cannot use springs.
tv1 is a tableview. Its expand property is set to True.
At the bottom is a HBox. Inside, from left to right, is a label labC, a boldface label labAverage, a spring, and a Quit button called bQuit whose text property is “Quit”.
As each number is entered the average is recalculated. Blank cells are skipped.
LabC shows the count and labAverage the average.
PublicNamesAsNewString[]PublicScoresAsNewFloat[]PublicSubbQuit_Click()QuitEndPublicSubForm_Open()Names=["Mereka AIKE","Ernest AIRI","John AME","Stanley ANTHONY","Natasha AUA","Veronica AUFA","John Taylor BUNA","Romrick CLEMENT","Philomena GAVIA","Richard GHAM"]tv1.Rows.Count=Names.countScores.Resize(Names.Count)tv1.Columns.Count=2tv1.Columns[1].Alignment=Align.RightEndPublicSubCalculateAverage()Dimi,nAsIntegerDimtAsFloatFori=0ToScores.MaxIfScores[i]=-1OrIsNull(tv1[i,1].text)ThenContinue'skip new but unfilled-in linesn+=1'number of scorest+=Scores[i]'totalNextIfn=0ThenReturnlabC.Text="N= "&nlabAverage.Text="Avg= "&Format(t/n,"#0.00")EndPublicSubtv1_Click()Iftv1.Column=1Thentv1.Edit'numbers column is editable; Enter goes downEndPublicSubtv1_Activate()'double-clicked a cellIftv1.Column=0Thentv1.Edit'edit a nameEndPublicSubtv1_Save(RowAsInteger,ColumnAsInteger,ValueAsString)tv1[Row,Column].text=ValueIfColumn=1ThenScores[Row]=ValueCalculateAverageElseNames[Row]=ValueEndifEndPublicSubtv1_Data(RowAsInteger,ColumnAsInteger)IfColumn=0Thentv1[row,0].text=Names[Row]IfRowMod2=0Thentv1[Row,Column].Background=&hDDDDFF'light bluetv1.Columns[0].Width=-1'Automatically set width based on contentsEndPublicSubtv1_Insert()tv1.Rows.Count+=1Scores.Add(-1)Names.Add("")tv1.MoveTo(tv1.Rows.max,0)tv1.EditEnd
Right at the start two public arrays are created. Names[] is a list of the student names. Scores[] is a list of their results. They match: the first mark goes with the first name, the second mark with the second name, and so on.
The tv1_Data(Row As Integer, Column As Integer) event fires every time a cell needs to be redrawn. It supplies you with Row and Column. It can be thought of as painting the cell.
There is a special consideration with the _DATA event: it does not paint cells that it doesn’t need to. This is great for displaying large numbers of lines. If there are 100,000 lines and you are only showing 15 of them, only the cells on those 15 lines fire the _DATA event, not all hundred thousand. Be careful if you used the _DATA event to put the numbers into the cells! There may be data in only 15 of those 100,000 cells. Here there is no problem, because we are typing the numbers ourselves and every time we finish typing a new number it is put into the cell in the _SAVE event. (tv1[Row, Column].text = Value). When we come to putting values in using the _DATA event from a database, though, we shall only put data into the cells we see. Then we have to remember to do calculations on the internally-held data, not on the displayed contents of cells. To get into good habits, I have used the DATA[ ] array to hold the scores, and this is used in the calculation of averages. If comes down to this: if you are sure all the data is in the cells, use them; if not, use the data where you know for sure it is.
The following lines create a contextual menu for the tableview with four entries:
PublicSubtv1_Menu()Dimmn,suAsMenu'main menu and submenumn=NewMenu(Me)'brackets contain the parent, the main windowsu=NewMenu(mn)As"MenuCopyTable"'submenu of mn; alias is MenuCopyTablesu.Text="Copy table..."'first submenu's textsu=NewMenu(mn)As"MenuCopyNames"su.Text="Copy names..."'second submenu's textsu=NewMenu(mn)As"MenuDeleteRow"su.Text="Delete Row"'third submenu's textsu=NewMenu(mn)As"MenuRefresh"su.Text="Refresh"'fourth submenu's textmn.PopupEndPublicSubMenuDeleteRow_Click()Names.Remove(tv1.Row)Scores.Remove(tv1.Row)tv1.Rows.Remove(tv1.Row)EndPublicSubMenuCopyTable_Click()'clicked the Copy Table menu itemDimzAsStringForiAsInteger=0ToNames.MaxIfScores[i]=-1ThenContinuez=If(IsNull(z),"",z&gb.NewLine)&Names[i]&gb.Tab&Scores[i]NextClipboard.Copy(z)Message("Table copied")EndPublicSubMenuCopyNames_Click()'clicked the Copy Names menu itemDimzAsStringForiAsInteger=0ToNames.MaxIfIsNull(Names[i])ThenContinuez=If(IsNull(z),"",z&gb.NewLine)&Names[i]NextClipboard.Copy(z)Message("Names copied")EndPublicSubMenuRefresh_Click()tv1.Clear'clear the datatv1.Rows.Count=Names.Count'reset number of rows to match Names[ ]ForiAsInteger=0ToNames.Maxtv1[i,0].Text=Names[i]tv1[i,1].Text=Scores[i]IfiMod2=0Thentv1[i,0].Background=&hFFDDFFtv1[i,1].Background=&hFFDDFFEndifNextEnd
The _Menu() event belongs to tv1, the tableview. This event fires when the object is right-clicked (to show a menu). The _Click() event belongs to TableviewMenu. What is that? It is the alias by which the menu su is known. Aliases make one think of secretive men in dark trenchcoats, but it is just the name by which it is known.
mn and su only exist for the duration of the popup menu because they are in the _Menu() event. As soon as you click any item on the popup menu, that sub finishes and mn and su disappear. Luckily, we have said that su is also known as TableviewMenu. The menu itself, when it was created with New, has that name. So any clicks the menu gets are handled by TableviewMenu_Click().
Menus—whether main menus or submenus or menu items— have several events, methods and properties. See Gambas help for:
This suspicious character is known by an alias, as menus are.
You can tell a menu to Close, Hide, Popup, Show or Delete.
You can tell the program to do things when the menu is clicked, after it hides or just before it shows. Usually it is when the menu is clicked that the menu gets to work.
Some properties are boolean (e.g. enabled, checked and visible), others are strings (e.g. text, name), pictures (picture) or variants, which are any type (tag).
There is another way to create a contextual menu for when you right-click the tableview: not in code as we have done, but at design time. You still have to write code to handle the menu item clicks, but you avoid all those New Menuitem statements like mn = New Menu(Me) and su = New Menu(mn) As "MenuCopyTable". Simply use the Menu Editor to create a new menu with all its menu items. This menu will appear on the menubar, so to avoid that happening make it invisible (see the Visible checkbox below).
This is an easier way to make a contextual menu. It works if you know what the menu is going to be before you start the application. If you need to make a menu depending on what is typed in, you have to create the menu in code and use the New operator.
There is no built-in sorting. I wish there were a method attached to each TableView like TableView_Sort(Column, Direction, NumericOrNot), but there isn’t. There is a good way to sort in the online wiki, here, but alas, it doesn’t sort correctly for columns containing numbers. 10 comes before 2, for example, because “1” is would come before “2” in a dictionary. The string “10” is less than the string “2” even though the number ten is greater than the number two.
(Thanks to fgores, Lee Davidson and Gianluigi of the forum for this method.)
So here is my method—somewhat agricultural, but it works. The idea is to go through the tableview row by row, gathering the cells in that row into a string with a separator between each, and putting the item that will determine the sorting right at the start of each. Sort the array, then unpack each row back into the tableview. In other words, pack each row into a string, sort the strings, and unpack each row. Use any rare and unusual character to separate the fields. Here a tilde (~) is used.
PublicSubtv1_ColumnClick(ColumnAsInteger)tv1.Save'Calls the Save event in case the user clicked col heading without pressing EnterSortTable(tv1,Column,tv1.Columns.Ascending,(Column=1))EndPublicSubSortTable(TheTableAsTableView,ColumnAsInteger,AscendingAsBoolean,NumericAsBoolean)DimzAsNewString[]DimyAsNewString[]DimsAsStringDimi,jAsIntegerFori=0ToTheTable.Rows.MaxIfNumericThens=TheTable[i,Column].text'next line pads it with leading zeross=String$(5-Len(s),"0")'So 23 becomes 00023, for example. Works up to 99999Elses=TheTable[i,Column].textEndifForj=0ToTheTable.Columns.Maxs&="~"&TheTable[i,j].textNextz.add(s)NextIfAscendingThenz.Sort(gb.Ascent)Elsez.Sort(gb.Descent)'sortFori=0Toz.Max'unpack the arrayy=Split(z[i],"~")Forj=1Toy.Max'skip the first item, which is the sort keyTheTable[i,j-1].text=y[j]'but fill the first columnNextNextEnd
You know the game: cards are arranged face down, you turn over a card, then you try to remember where you have seen its match. It’s somewhere...thinks...yes, it was here! You turn it over to find … no match! It was somewhere else. You turn the cards over and your friend takes a turn. If you do turn over matching cards, you take the cards. Whoever has the more cards at the end wins.
Concentration, or "Memory", played with cards
In this version, there is only one player and you are racing against the clock to match all the cards.
The form is 508 wide and 408 high, but that is what I needed when I used Cooper Black for the font and +12 for the increased font size. Cooper Black has nice big black letters.
The File menu has three items, Give Up, New Game and Quit.
The form has its arrangement set to Fill so that the solitary gridview gv1 fills the whole window. It shouldn’t be resized, though, so set resizeable to False.
What we will do is have a 6x6 grid. All squares will be pale yellow. In memory is a kind of mirror image of the grid in an array called z . Arrays we have used up to now have been lists. A list is a series of items in a line. Lines are one-dimensional. The one dimension is their length. Grids are 2-dimensional and their two dimensions are length and width. For gridviews there are rows and columns. In our case z has rows and columns just as the gridview gv1 has. Public z As New String[6, 6] will create z as this in-memory grid. The top left cell of the gridview is gv1[0,0]. The top left corner of z is z[0,0]. The bottom right cell of the gridview is gv1[5,5], and the bottom right corner of z is z[5,5]. 36 cells in the gridview, like the 36 memories in z, are arranged in rows and columns.
The gridview shows the “cards” as we turn them over. The array z has the “underneath sides of the cards”. Pictures of your favourite relatives would be nice, but for now they will have big, Cooper Black letters, one on each.
You click a grid cell. The card turns over: we show the letter that we have hidden in z. If we kept doing this every time we clicked a grid cell there would be no game. We would just gradually reveal all the letters. So when you show a letter we raise a flag that says “One letter is showing”. If that flag is up, the next time you click a cell we shall know that a second card has been turned over and it is time to check for a match. No match? Hide the letters and lower the flag—one letter is NOT showing. If we have a match perhaps it is the very last card and you have finished the game. Or perhaps it is not the very last card, and you can leave the cards turned over (their letters showing) and play on, remembering to lower that flag because the next click will again be clicking the first-card-of-the-pair. The flag is a boolean (true/false, hat on/hat off) variable called OneShowing.
How do we know the game is finished? Every time we have a match, add 2 to a running total of how many cards are out of the game. When that total reaches 36, all cards have been matched. The variable that keeps count is called TurnedOver and it is an integer. It is a public (or private, as in “private to the form”—it doesn’t matter) variable, declared right at the start with all the other variables that need to exist for the duration of the form and not disappear when a sub finishes.
There is a timer included. The built-in function Timer() represents the number of seconds since the application started. As soon as you make your first click the time is stored in StartTime. How long you took to play is put into the variable secs.
Dim secs As Integer = Timer() - StartTime
The game board is set up in the Initialise sub. It zeroes the things that have to be zeroed. It calls on GetRandomLetters to make a list called s[] of the letters that will be distributed to the cells of z with its 6 rows and 6 columns. s needs to have 18 random letters, each repeated so there are matching pairs. It has 36 items altogether. Here are the screenshots and the code.
Looking for Matches...“Give Up” shows where they all were.The 508 x 458 form. Arrangement property = Full. gv1 has expand set to True.
ConstnRowsAsInteger=6ConstnColsAsInteger=6ConstPaleYellowAsInteger=&hFFFFAAPublicTurnedOverAsIntegerPubliczAsNewString[nRows,nCols]PublicsAsNewString[]PublicOneShowingAsBooleanPublicFirstRowAsIntegerPublicFirstColumnAsIntegerPublicStartTimeAsFloatPublicSubForm_Open()Dimi,jAsIntegergv1.Columns.count=nColsgv1.Rows.Count=nRowsgv1.Background=Color.DarkBlueFori=0Togv1.Columns.maxgv1.Columns[i].Alignment=Align.Centergv1.Columns[i].Width=84Forj=0Togv1.Rows.maxgv1[i,j].Padding=16NextNextInitialiseEndPublicSubInitialise()Dimi,j,n,nCol,nRowAsIntegerDimcAsStringnCol=gv1.Columns.maxnRow=gv1.Rows.MaxGetRandomLetters(18)'each letter twiceFori=0TonRowForj=0TonColc=s[n]z[i,j]=cgv1[i,j].ForeGround=Color.Blackgv1[i,j].Text=""gv1[i,j].Background=Color.DarkBluen=n+1NextNextTurnedOver=0EndPublicSubGetRandomLetters(CountAsInteger)Dimi,p,r1,r2AsIntegerRandomize'different random numbers every times.clearp=Rand(Asc("A"),Asc("Z"))'start with any letterDoUntils.count>=2*Counts.Add(Chr(p))s.Add(Chr(p))'other one in the pairp+=1Ifp>Asc("Z")Thenp=Asc("A")'back to the startLoopFori=0Tos.Count'c.shuffle() 'When I update to 3.13 I can use this!r1=Rand(0,s.max)r2=Rand(0,s.max)Swaps[r1],s[r2]NextEndPublicSubMenuNew_Click()InitialiseEndPublicSubMenuQuit_Click()QuitEndPublicSubgv1_Click()IfTurnedOver=0ThenStartTime=Timer'begin timing from the first clickIfOneShowingThengv1[gv1.row,gv1.Column].Background=Color.DarkBluegv1[gv1.row,gv1.Column].Foreground=Color.Whitegv1[gv1.row,gv1.Column].Text=z[gv1.row,gv1.Column]gv1.RefreshWait'finish pending operations and do the refreshEvaluate(gv1.row,gv1.Column)ElseFirstRow=gv1.rowFirstColumn=gv1.Columngv1[FirstRow,Firstcolumn].Background=Color.DarkBluegv1[FirstRow,Firstcolumn].Foreground=Color.Whitegv1[FirstRow,Firstcolumn].Text=z[FirstRow,FirstColumn]OneShowing=TrueEndifEndPublicSubEvaluate(rowAsInteger,columnAsInteger)Ifz[FirstRow,FirstColumn]=gv1[row,column].TextThen'a matchTurnedOver+=2IfTurnedOver=nRows*nColsThenDimtAsStringt=TheTime()Message("Well done!<br>You took "&t)InitialiseElseWait0.5gv1[FirstRow,FirstColumn].Text=""gv1[row,column].Text=""gv1[FirstRow,FirstColumn].Background=PaleYellowgv1[row,column].Background=PaleYellowEndifElse'no matchWait1'secondgv1[FirstRow,FirstColumn].Text=""gv1[row,column].Text=""gv1[FirstRow,FirstColumn].Background=Color.DarkBluegv1[row,column].Background=Color.DarkBlueEndifOneShowing=FalseEndPublicSubTheTime()AsStringDimsecsAsInteger=Timer()-StartTimeDimhAsInteger=secs/60/60Secs-=h*60*60DimmAsInteger=secs/60Secs-=m*60ReturnIf(h>0,Str(h)&"h ","")&If(m>0,Str(m)&"m ","")&Str(secs)&"s"EndPublicSubMenuGiveUp_Click()ForiAsInteger=0TonRows-1ForjAsInteger=0TonCols-1Ifgv1[i,j].Text=""Thengv1[i,j].ForeGround=Color.Redgv1[i,j].Text=z[i,j]gv1[i,j].Background=Color.DarkRedEndIfNextNextEnd
If you prefer to work with clicking pictures, you will need a folder called Pix located in your Pictures folder. You need to put 18 pictures in it (jpg or png).
Now, where is the other Snoopy?“Give up” shows where they all were.
In the code that follows, the picture files are read into an array of images. Images and Pictures in Gambas differ in what part of memory they are stored in. Images, unlike pictures, can be stretched to fit in an area with any given width and height. So the images in the array of eighteen are, when one is needed, put into a single Image called Img, stretched to fit one of the grid cells. The resulting image, now the right size, is converted to a picture using the Picture method that images have.
To show an image the cell’s Picture property is set to the converted image. To hide it the picture property is set to Null. This happens when you click on a cell.
There is a corresponding two-dimensional (rows/columns) array of picture names. To see if there is a match, the names of the pictures in the two clicked-on cells are compared.
It is time to congratulate the winner when 18 cells have been correctly matched (two at a time).
ConstnRowsAsInteger=6ConstnColsAsInteger=6ConstPaleYellowAsInteger=&hFFFFAAPublicTurnedOverAsIntegerPubliczAsNewString[nRows,nCols]'names of the picturesPublicImagesAsNewImage[nRows,nCols]'the images themselvesPublicImgAsImagePublicsAsNewString[]PublicOneShowingAsBooleanPublicFirstRowAsIntegerPublicFirstColumnAsIntegerPublicStartTimeAsFloatPublicSubForm_Open()Dimi,jAsIntegergv1.Columns.count=nColsgv1.Rows.Count=nRowsgv1.Background=PaleYellowFori=0Togv1.Columns.maxgv1.Columns[i].Alignment=Align.Centergv1.Columns[i].Width=84NextInitialiseEndPublicSubInitialise()Dimi,j,n,nCol,nRowAsIntegerDimcAsStringnCol=gv1.Columns.maxnRow=gv1.Rows.MaxGetRandomLetters'each picture twiceFori=0TonRowgv1.Rows[i].Height=70Forj=0TonColc=s[n]z[i,j]=cgv1[i,j].Picture=Nullgv1[i,j].Background=Color.DarkCyann=n+1NextNextTurnedOver=0EndPublicSubGetRandomLetters()Dimi,j,nAsIntegerDimpathAsString=User.Home&/"Pictures/Pix/"'must be 18 pictures in hereDimcellWAsFloat=gv1[0,0].WidthDimcellHAsFloat=gv1[0,0].HeightDimscaleAsFloat=Min(CellW,CellH)IfNotExist(Path)ThenMessage("Please create a folder called Pix in your Pictures folder.<br>Put 18 pictures in it.")QuitEndifs=Dir(path,"*.png")s.Insert(Dir(path,"*.jpg"))Ifs.Count<18ThenMessage("Please put 18 pictures in the Pix folder inside your Pictures folder.<br>There were only "&s.Count)QuitEndifs.Insert(s)'second copys.ShuffleFori=0Togv1.Rows.MaxForj=0Togv1.Columns.MaxImages[i,j]=Image.Load(path&s[n])n+=1NextNextEndPublicSubMenuNew_Click()InitialiseEndPublicSubMenuQuit_Click()QuitEndPublicSubgv1_Click()IfTurnedOver=0ThenStartTime=Timer'begin timing from the first clickIfOneShowingThengv1[gv1.row,gv1.Column].Background=Color.WhiteImg=Images[gv1.row,gv1.Column].stretch(70,70)gv1[gv1.row,gv1.Column].Picture=Img.Picturegv1.RefreshWait'finish pending operations and do the refreshEvaluate(gv1.row,gv1.Column)ElseFirstRow=gv1.rowFirstColumn=gv1.Columngv1[FirstRow,Firstcolumn].Background=Color.WhiteImg=Images[FirstRow,FirstColumn].stretch(70,70)gv1[FirstRow,Firstcolumn].Picture=Img.PictureOneShowing=TrueEndifEndPublicSubEvaluate(rowAsInteger,columnAsInteger)Ifz[FirstRow,FirstColumn]=z[row,column]Then'a matchTurnedOver+=2IfTurnedOver=nRows*nColsThenDimtAsStringt=TheTime()Message("Well done!<br>You took "&t)InitialiseElseWait0.5'half secondgv1[FirstRow,FirstColumn].Picture=Nullgv1[row,column].Picture=Nullgv1[FirstRow,FirstColumn].Background=PaleYellowgv1[row,column].Background=PaleYellowEndifElse'no matchWait1'secondgv1[FirstRow,FirstColumn].Picture=Nullgv1[row,column].Picture=Nullgv1[FirstRow,FirstColumn].Background=Color.DarkCyangv1[row,column].Background=Color.DarkCyanEndifOneShowing=FalseEndPublicSubTheTime()AsStringDimsecsAsInteger=Timer()-StartTimeDimhAsInteger=secs/60/60Secs-=h*60*60DimmAsInteger=secs/60Secs-=m*60ReturnIf(h>0,Str(h)&"h ","")&If(m>0,Str(m)&"m ","")&Str(secs)&"s"EndPublicSubMenuGiveUp_Click()ForiAsInteger=0TonRows-1ForjAsInteger=0TonCols-1Ifgv1[i,j].Picture=NullThenImg=Images[i,j].Stretch(70,70)gv1[i,j].Picture=Img.Picturegv1[i,j].Background=Color.WhiteEndIfNextNextEnd
Characters (letters, digits, punctuation symbols) are stored in a computer’s memory by numbers. The most widely used system is ASCII, American Standard Code for Information Interchange. It was developed in the United States, and Wikipedia tells me the governing body prefers to call it US-ASCII because it uses the American dollar sign ($) and the Latin alphabet. Whenever you hit a key on the keyboard one of those code numbers goes into the computer. Even the non-printing characters have ASCII codes. The spacebar is 32. Hit the Delete key and 127 would go in. The Backspace key sent the number 8. To confuse you, the ASCII code for the digit ‘1’ is 49. What the computer does with these code numbers is up to the application. And you are writing the applications. In the above program, type what you like and nothing happens at all (except for CTRL-G which I have for “Give Up”, CTRL-N which is “New Game” and CTRL-Q, which is the shortcut for Quit).
TypeWriter
On the old manual typewriters at the end of a line you had to flick a lever and the roller with the paper going around it would zip back to the start of the line (Return) and pull the paper up a line (Linefeed) ready to start typing the next line. Return is 13. ASCII 13 is also Control-M (written ^M) and in programming languages is sometimes written \r. Linefeed is 10 and is also Control-J (^J). There is a Formfeed control, Control-L, that used to go to a new page (ASCII 12). You wouldn’t remember manual typewriters unless you spend time in museums, but for me it is like yesterday (sigh). Nowadays ASCII is largely replaced by Unicode. ASCII was limited to 128 characters. Unicode can display 137,993 characters, says Wikipedia—enough for all sorts of non-English characters and all the emojis you could ever want.
Even the original ASCII gave problems for people who spoke languages other than English. Wikipedia has the amusing example of ‘a Swedish programmer mailing another programmer asking if they should go for lunch, could get "N{ jag har sm|rg}sar" as the answer, which should be "Nä jag har smörgåsar" meaning "No I've got sandwiches" ’ and he or she would just have to put up with it.[1]
Radio buttons are like the buttons on the old cassette players. When you press down on one the others pop up. They are used for selecting one option among many.
If you have some radio buttons in a form, only one can be highlighted. Click one and the others clear. Even in code if you set one button’s highlight the others will unhighlight by themselves. The value property (boolean) indicates if the button is highlighted. When you click a radio button rb1rb1.value = true happens automatically. When you click another button rb1.value = false happens automatically.
You might need two sets of radio buttons. To keep them separate, create them in a panel or some other container. Put the panel there first, and then make radio buttons in it, or select all the buttons you want to work together, right-click, and choose Embed in a container.
Another trick is to make them all share their events. Click any of the buttons and one and only one _Click event will fire. This avoids writing separate _Click handlers for each button. One handler does them all. But how do you know which button was clicked? Your one handler will probably want to do something based on which button was selected. This is where Last comes in. Last is the very last object that something happened to or that did something.
There are two sets of buttons, with each set in their own panel. rbRoad, rbSea and rbAir are in Panel1. rbApple, rbOrange and rbPear are in Panel2. The panels are the parents of their buttons. The buttons are their children.
The Group property for the road, sea and air buttons is set to rbTransport. It is as if they are acting like they are one single radio button, rbTransport.
The Group property for the apple, orange and pear buttons is set to rbFruit. It is as if they are one radio button, rbFruit.
Double-click one of the transport buttons (any one). You will find yourself writing a handler for the rbTransport group of buttons. Likewise, double-click one of the fruit buttons and you will find yourself writing a handler that rbApple, rbOrange and rbPear all respond to.
PublicSubrbTransport_Click()Message("You choose to travel by "&Last.text)EndPublicSubrbFruit_Click()Message("I like "&LCase(Last.text)&"s too!")End
The LCase function makes the text inside the brackets lower case. Run the program and click on buttons.
In the next section, we add the ability to save settings to this program. Whatever state the buttons are in when we close the application will be the state to which they are restored when the application runs next time.
Gambas provides a neat way to save settings. Settings can be the path to the last data file, so it does not have to be relocated the next time the program starts. They can be anything the user typed or chose that you want to remember for next time. Here we shall save the selected radio buttons.
First, make sure the Settings component is enabled as part of your project. After starting a new QT graphical project, select Project Menu > Properties…, look through for the gb.settings component and tick it:
Use the same form as on the previous page (RadioButtons) with the fruit and transport buttons, but change the code to this:
Run the program. Select a transport and fruit. Close the program. Run it again: your choices have been restored.
You could have your settings saved when the form closes. Gambas wiki has this example, showing how you can restore the window to whatever place it was last dragged to and whatever size it was resized to when last the program ran:
Where are these settings actually stored? In your home folder is a hidden folder for settings called .config . In Linux any file or folder whose name starts with a dot is hidden. Look in .config for the Gambas3 folder. In it you will find a text file with the same name as your program. Open it and you will see the settings file.
The settings text file for the Radio Buttons application
Settings are neatly arranged under headings. Now you can see the significance of the string that has the slash in it: the first item is the heading. Settings["Radiobuttons/Fruit"] is the Fruit setting under the Radiobuttons heading.
You need to be careful: the very first time you run your program there may not be a settings file. If your form opens and looks for a particular setting when no settings file exists there will be problems. Test for empty (null) strings.
Saving a colour, a checkbox and the contents of a TableView
On the form is a checkbox cbSurnameFirst, a panel Panel1, a label with the text “Choose colour:”, a colorbutton ColorButton1, a label Label1 whose text is “Fill”, colour blue and underlined, and a tableview tv1.
Run the program. Fill the tableview with random letters. Choose a colour. Highlight the completely useless button “Surname first”. Close the program. Run the program again. Settings are restored.
There is a special form of the IF...THEN...ELSE statement that saves writing several lines of code. It is in the form of a function. These two are equivalent:
In the one-line statement, the If(IsNull(colour), &hFFFFFF, colour) is one single thing. It is a number representing a color. Which colour? In the brackets are three items: a test that is either true or false, the answer if the test comes up true and the answer if the test comes up false. The pattern is if( TrueOrFalseThing, ValueIfTrue, ValueIfFalse). &hFFFFFF is the hexadecimal number for White (all red, green and blue LED lights fully on).
This is a sample settings file. On the left the checkbox is unchecked. On the right, the checkbox is checked.
Programs tend to become large as more features are added to them. More menu items mean more menu handlers. More buttons, more lists, more tables—all mean more subs. There needs to be a way to organise them, and there is. The files in a computer are organised into folders. The subs in a program are organised into modules and classes.
Modules are like containers. Classes are like animals of various species.
You can put what you like into containers. You can collect all the subs that are related in some way and put them into a module. For example, you might make a module called Time and put in it all the bits of program related to times and dates. In it you might put that great function you wrote to work out a person’s age given their date of birth. You called it
Public Sub AgeThisYear(DOB as date) as string
And with it you could put that function that takes how many seconds you took to complete a puzzle and convert it into minutes and seconds format:
Public Sub TidyTime(Secs as integer) as string
They could go into the Time module to save cluttering up the form’s code. There will be enough event handlers to fill it up without these functions as well. You cannot move those event handlers into a module. They have to be in the form’s code, waiting for something on the form to do something to make them fire.
How do you call on subs that have been put into a module? You have to refer to the module they have been put in, and the name of the sub. It is like having a crowd of people all gathered together in a park. You can call out “John!” or “Mary!” and John or Mary will step forward. Once you start putting people into houses, though, it has to be “HollyCottage.John” or “FernyVale.Mary”. There might be several Johns or Marys, for one thing. So we would refer to
Time.AgeThisYear(“1/6/1955”)
and
Time.TidyTime(258)
Anything in a module is available throughout your program. Modules are like boxes or folders or filing cabinets: just places you can park your subs. To call them, put the module name, a dot, then the sub.
Classes, though, are like kinds of animals. In the animal world, species are grouped into genuses, genuses into families, and so on up to Kingdom (and now one higher level, Domain), which used to be Animal or Plant but now includes others (Monera which is mainly bacteria, Protists which include algae and protozoans, and Fungi which is mushrooms, moulds and yeasts). Every animal and plant has a two-part name made up of Genus and Species. “Genus” means “General” and “Species” means “Specific”. My name is, let us say, Luke Bilgewater. There are many different individuals in the Bilgewater family, but only one Luke Bilgewater. Luke is the specific name and Bilgewater is the general or generic name. The classification system goes this way:
DOMAIN → KINGDOM → PHYLUM → CLASS → ORDER → FAMILY → GENUS → SPECIES
You will notice that “Class” comes in there. In programming, classes are things that you can have examples of. A tableview is a class. You can have many tableviews in a program. A button is a class. You can have many buttons. A menu is a class. You can have many menus. A form is a class. You can have many forms.
You can also derive classes from other classes.
Let’s take the horse. Wikipedia says, “At present, the domesticated and wild horses are considered a single species, with the valid scientific name for the horse species being Equus ferus”. Imagine a horse with wings. It would have everything that regular horses have (properties, like tails and hooves) and can do everything that regular horses do (methods, like gallop and neigh) and respond to things regular horses respond to (events, like approaching a water trough or getting saddled up). However, in addition to these, it will have wings. This new class, Equus Pegasus, will inherit everything that an Equus has, but will also have Public Sub Wings() … End specific to this special type of Equus.
Classes (unlike modules) can have as many real live examples as you want. Each example will have its own wings. Each will have its own name. “Make a new example of one of these kinds of animals” is, in programming language, “instantiate” or “make a new instance”.
A class is an abstract thing (like a type of animal). You need instances of the class to have anything you can work with.
In saying that a class definition is like a blueprint, that is the usual case. If you want the class to exist as a one-of-a-kind animal, you can do that by making it Static. Gambas has static classes. They are classes that are always there. Modules are static classes: always there, and you only have one of them.
An example of a static class is Key. In the online help it says, about Key, “This class is used for getting information about a key event, and contains the constants that represent the keys. … This class is static.” It is always there; it is a once-off thing; refer to it as if it were the name of a beast, not just the kind of beast that it is. So the key that was just typed is Key.Text and the number code for that key is Key.Code . Just as sure as horses have legs, Key has several constants you can refer to, like Key.Enter and Key.Return and Key.Del and Key.Esc that are the code numbers for those keys. And just as sure as horses can have a saddle on or a saddle off, there is the property Key.Shift and Key.Control that can be up or down, that is to say, true or false.
Let’s take a tableview and give it wings. Our new class will be everything that a tableview is, but with the additional ability to locate lines by typing in a few of the letters. It is our SearchBox again, only this time we shall make it a class. Then we can make as many new SearchBoxes as we like. We only need design the prototype for the new car; after that we can have as many rolling off the assembly line as we like.
The File Menu has two entries, Go Down and Quit. Don’t give Go Down a keyboard shortcut. (If typed while in a cell you can find yourself in an endless loop—nothing happens, nothing responds.) The menu items are called MenuGoingDown and MenuQuit respectively.
The menu item Go Down uses its checkbox. If ticked, searching for names by typing is switched off. Enter moves the cursor down to the next cell below as usual. If unticked (the program starts this way), type a few letters to locate a person’s name and press Enter to enter the score. Then press Enter again to leave the cell and be ready to search for the next name. The tableview property that is critical here is NoKeyboard. The menu item sets or unsets it.
Steps:
Start a new graphical application.
Create a new class, make it Exported and call it “SearchBox”. (Rt-click the Sources folder > New > Class...)
Enter the line INHERITS TableView right at the top of the class. The class is exported and inherits everything that TableViews have.
Press F5 to run the application. Quit the program straight away. The SearchBox class appears in the toolbox.Now there should be a SearchBox among the classes. Drag one onto the form FMain. You have just made a new instance. You can peel off as many copies as you want. For the moment, we only need the one. Now we teach our horse to fly. This code goes in the SearchBox class:
Export'Without this you will not see the class in the toolbar of classesInheritsTableViewPrivatessAsStringPublicSearchColumnAsIntegerEventEnterOnLinePublicSubCheckKey()SelectCaseKey.CodeCaseKey.Esc,Key.BackSpace,Key.Delss=""Me.UnSelectAllCaseKey.Enter,Key.ReturnRaiseEnterOnLine'action on pressing Enterss=""CaseKey.TabSearchDownCaseKey.BackTabSearchUpCaseElsess&=Key.TextSearchDownEndSelectEndPrivateSubSearchUp()Dimi,StartAsIntegerIfMe.Rows.Selection.Count=0ThenStart=-1ElseStart=Me.Rows.Selection[0]'the selected lineFori=Start-1DownTo0IfInStr(LCase(Me[i,SearchColumn].text),LCase(ss))ThenMe.Rows.Select(i)ReturnEndifNextFori=Me.Rows.maxDownToStartIfInStr(LCase(Me[i,SearchColumn].text),LCase(ss))ThenMe.Rows.Select(i)ReturnEndifNextEndPrivateSubSearchDown()Dimi,StartAsIntegerIfMe.Rows.Selection.Count=0ThenStart=-1ElseStart=Me.Rows.Selection[0]Fori=Start+1ToMe.Rows.Max'if no selected line, start at top, else start at next lineIfInStr(LCase(Me[i,SearchColumn].text),LCase(ss))>0ThenMe.Rows.Select(i)ReturnEndifNextFori=0ToStart'if no more occurrences, you will end up at the line you are onIfInStr(LCase(Me[i,SearchColumn].text),LCase(ss))>0ThenMe.Rows.Select(i)ReturnEndifNextEndPublicSubHandleClick()IfMe.Column=SearchColumnThenReturn'searchable column is not editable by clickingss=""Me.EditEnd
This above code is now part of all searchboxes. It still has to be called upon at the right times. If not, it will never get done. So here is the code for the main form FMain:
PublicSubsb1_EnterOnLine()sb1.Column=1sb1.EditEndPublicSubForm_Open()DimNamesAsNewString[]DimiAsIntegerNames=["Mereka AIKE","Ernest AIRI","John AME","Stanley ANTHONY","Natasha AUA","Veronica AUFA","John Taylor BUNA","Romrick CLEMENT","Philomena GAVIA","Richard GHAM","Gerard BUZOLIC","John HEARNE","Thomas EDISON"]sb1.Rows.Count=Names.countsb1.Columns.Count=2Fori=0ToNames.Maxsb1[i,0].text=Names[i]IfiMod2=0Thensb1[i,0].Background=&hDDDDFFsb1[i,1].Background=&hDDDDFFEndIfNextsb1.Columns[0].Width=140'-1 for max needed widthsb1.Mode=Select.Singlesb1.NoKeyboard=True'start with sb1 selecting the line when Enter is pressed in a cellsb1.Expand=Truesb1.SetFocusEndPublicSubMenuQuit_Click()'Yes, I put in a Quit menuitem in a File menu.QuitEndPublicSubsb1_KeyPress()sb1.CheckKey()EndPublicSubsb1_DblClick()sb1.Edit'to edit the names in the searchable column, double-click one of themEndPublicSubsb1_Save(RowAsInteger,ColumnAsInteger,ValueAsString)sb1[Row,Column].text=ValueEndPublicSubsb1_Click()sb1.HandleClickEndPublicSubMenuGoingDown_Click()MenuGoingDown.Checked=NotMenuGoingDown.Checkedsb1.NoKeyboard=Notsb1.NoKeyboardEnd
This horse knows how to fly. This tableview, now known by the illustrious and noble name of SearchBox, knows how to search for occurrences of what you type.
You talk to the horse when the horse is listening. You talk to the SearchBox when it gives you events that you can intercept. Our particular SearchBox, sb1, gives us all the events that tableviews do, and one more. It has a homemade event EnterOnLine.
The horse does what you tell it if you use words it understands. When the form gets a keypress event from the searchbox, tell it to CheckKey. SearchBox knows how to check your key. SearchBox will happily scan upwards or downwards looking for the next occurrence of what you typed. If you are happy with the selected line it presents to you, it will notify you with the EnterInLine event. Otherwise it adds the key to the string of letters you have already typed and does some more searching, starting with the next line.
EnterOnLine is raised when the key you type gets checked. If you typed Enter or Return, the EnterOnLine event occurs. In the main window you decide what you are going to do about it (if anything). In our case, it means we have found the line we are after and we want to type in the second column.
In a sense, you talk to the class through events and the class talks to you through events that it itself raises.
Homemade events that your classes give you might be like a simple greeting (“Hello!”) or they can convey parameters (“Hello, I’ll be there in 5 minutes!”). Our EnterOnLine might tell us which line you pressed Enter on. Then the Event definition would then read
When a line is highlighted its Row property is set to that row anyway, so it is not necessary. Like, why give a command to move to a row when you are already at that row? So the LinNum parameter is not necessary, but the horse talks to you through events it raises and it could tell you what line you are on when it sends you an event if you wanted it to.
If there are things to do when a new instance of a class is created, like setting up how many rows and columns there should be in a tableview or the names of the column headings, the place to do it is in a special sub called _new() . Whenever a new example of the class is made with the New operator, this event is called.
In the SearchBox program above, we can take all the setting up that is done in the Form_Open() event handler and move it into the class itself. Delete the Form_Open() event, and in the SearchBox class put the code there. When you run the program it works exactly the same.
Now the class sets itself up. The form does not have to set up each one.
PublicSub_new()DimNamesAsNewString[]DimiAsIntegerNames=["Mereka AIKE","Ernest AIRI","John AME","Stanley ANTHONY","Natasha AUA","Veronica AUFA","John Taylor BUNA","Romrick CLEMENT","Philomena GAVIA","Richard GHAM","Gerard BUZOLIC","John HEARNE","Thomas EDISON"]Me.Rows.Count=Names.countMe.Columns.Count=2Fori=0ToNames.MaxMe[i,0].text=Names[i]IfiMod2=0ThenMe[i,0].Background=&hDDDDFFMe[i,1].Background=&hDDDDFFEndIfNextMe.Columns[0].Width=140'-1 for max needed widthMe.Mode=Select.SingleMe.NoKeyboard=True'start with sb1 selecting the line when Enter is pressed in a cellMe.Expand=TrueMe.SetFocusEnd
A database is a file on the hard drive that has a structure to it so that it can hold large amounts of information and access it quickly.
SQLite is one type of database. It was written by Dwayne Richard Hipp (born 1961 in North Carolina). It was first released in August 2000. It is public domain, meaning anyone can use it free of charge. Google Chrome, Firefox, the Android operating system for smartphones, Skype, Adobe Reader and the iPhone all use SQLite. It’s just nice. And you pronounce it “S Q L Lite”, so saith Wikipedia.
Databases store information in tables. Gambas has a tableview. This, too, has rows and columns. You can think of a database table as an invisible tableview in the database file.
Rows are called Records. Columns are called Fields.
For example, a teaching might have a database with a Students table. In that table there is a row for every student. Looking across the row you see StudentID, FirstName, LastName, Sex, DateOfBirth, Address, PhoneNumber. These are the fields. They are the columns.
StudentID
FirstName
LastName
Sex
DateOfBirth
Address
PhoneNumber
2019001
Mary
Smith
F
2008-06-23
21 Holly Crt, Bundaberg
07324657
2019002
Jim
Jones
M
2003-02-19
14 Primrose St, Bundaberg
07123456
2019003
Lucy
Watkins
F
2003-10-05
5 Flower St, Bundaberg
07938276
This could be a TableView, or a Table in a Database file.
Every database table has to have a Primary Key. Every record must have a unique value for this field: one that no one else shares. The simplest is to call it RecID and number 1, 2, 3... etc. In the table above, the primary key is going to be the StudentID and it is an integer. The first four digits are the year of enrolment. (We could have another column for YearOfEnrolment and just use a sequence number for the StudentID.)
In SQLite all data is stored as strings, even though you might specify some columns as integers, others as strings and others as dates. SQLite is very forgiving: you can put things that aren’t numbers into integer columns and so on, but try not to. Empty cells are NULL. Try to avoid those, too. When you make a new blank record, initialise values to the empty string, “”.
SQLite is a component (optional part) of Gambas. There is also a Database access component. On the Project Menu > Properties… > Components page, be sure to tick gb.db and gb.db.sqlite3. Without these components in your project you will get errors as soon as you try to run your program.
You send messages to SQLite and it sends answers back to you using a special language called SQL (“S Q L” or “sequel”, pronounce it either way.) This means learning another language, but the simple statements that are used most frequently are not difficult to learn. They are the only ones I know, anyway. SQL was invented by Donald D. Chamberlin and Raymond F. Boyce and first appeared in 1974. SQL is so universal that everyone who writes databases knows of it. It is an international standard. SQLite is one implementation of it.
For example, you might send a message to SQLite saying
SELECT*FROMStudents
This says, “select everything from the Students table”. This gives you the whole table. Or you might only want the students who are male:
SELECT*FROMStudentsWHERESex='M'
Perhaps you want everyone, but you want the females first and the males second:
SELECT*FROMStudentsORDERBYSex
That will get the females first, because “F” comes before “M”. The females will all be in random order and likewise the males unless you write
This returns a table to you with males first (alphabetically by surname) followed by females (alphabetically by surname).
You might only want the students names, so you could write
Perhaps you want only those students who were enrolled in 2019. Now, this is part of the StudentID. You want only those students whose StudentID number starts with “2019”. You use a “wildcard”. The percent (%) sign means “anything here will do”.
When you send these SELECT statements to the database, SQLite will send you back a table. Gambas calls it a RESULT.
Suppose you have a database called db1 (as far as Gambas is concerned) and it is attached to MyStudentDatabase.sqlite which is the actual database file on your hard drive. You need a result to store the reply:
DimresasResultres=db1.exec("SELECT * FROM Students")
res has the information you asked for. You might want to print the information, or show it in a tableview, or hold it internally in arrays so you can do calculations on it. You need to cycle through the records thus:
Whileres.Available'do something with res!FirstName, res!LastName and res!DateOfBirth etcres.MoveNextWend
For displaying information in a tableview there is a special event that is triggered each time a cell has to have its contents painted on the screen. It is particularly useful if your set of records is large. The tableview does not have to hold all the information from all the records in itself. It can get the information as it needs it for when it has to be displayed. Be a little careful here: if you are depending on all the information being in the tableview, it may or it may not be all there. This is an example of using the _Data event, getting the information from the result table res when it is needed to display a particular cell in the tableview:
Notice the use of TableView1.Data.Text , which represents the text in the cell.
Notice we have result.MoveTo to go to a particular record, result.MoveNext if we are stepping through them one at a time, and result.Available to check to see if there is another record to MoveNext to. Useful in setting the number of rows to have in your tableview is result.RecordCount.
Besides accessing the information in the database, with databases you want to be able to:
Add records
Delete records
Modify records
All but the simplest databases have more than one table in them. Tables can be linked to each other, so records can have signposts in them to indicate lines in other tables that apply. The signpost is the record ID or other primary key of a record in another table. For example, a database of political candidates might have a signpost to the party they belong to. SQL is so smart it can look up the two tables at once to provide you with the information you need, for example this ‘join’ of two tables. (Candidates are in a particular party, and it is the parties that have policies on a variety of issues.)
You need a form with a tableview called tv1. Make it long and thin, as it has 2 columns.
The code is
' Gambas class filePrivatedb1AsNewConnectionPrivatersAsResultPublicSubSetupTableView()tv1.header=GridView.Horizontaltv1.grid=Truetv1.Rows.count=0tv1.Columns.count=2tv1.Columns[0].text="RecID"tv1.Columns[1].text="Value"tv1.Columns[0].width=55tv1.Columns[1].width=55EndPublicSubCreateDatabase()db1.Type="sqlite"db1.host=User.homedb1.name=""'delete an existing test.sqliteIfExist(User.home&"/Test.sqlite")ThenKillUser.home&"/Test.sqlite"Endif'create test.sqlitedb1.Opendb1.Databases.Add("Test.sqlite")db1.CloseEndPublicSubMakeTable()DimhTableAsTabledb1.name="Test.sqlite"db1.OpenhTable=db1.Tables.Add("RandomNumbers")hTable.Fields.Add("RecID",db.Integer)hTable.Fields.Add("Value",db.Integer)hTable.PrimaryKey=["RecID"]hTable.UpdateEndPublicSubFillTable()DimiAsIntegerDimrs1AsResultdb1.Beginrs1=db1.Create("RandomNumbers")Fori=1To10000rs1!RecID=irs1!Value=Rand(10,99)rs1.UpdateNextdb1.CommitCatchdb1.RollbackMessage.Error(Error.Text)EndPublicSubReadData()'read the databaseDimSQLAsString="SELECT * FROM RandomNumbers"rs=db1.Exec(SQL)EndPublicSubForm_Open()SetupTableViewCreateDatabaseMakeTableFillTableReadDataEndPublicSubForm_Activate()'change the rowcount of the gridview from 0 to the number of records.'This triggers the data handling eventtv1.Rows.Count=rs.CountEndPublicSubtv1_Data(RowAsInteger,ColumnAsInteger)rs.moveTo(row)IfColumn=0Thentv1.Data.Text=rs!RecIDElsetv1.Data.Text=rs!Value'If Column = 0 Then tv1.Data.Text = Str(rs["RecID"]) Else tv1.Data.Text = Str(rs["Value"])'Either of these two lines will do it.EndPublicSubForm_Close()db1.CloseEnd
When you work with a database a temporary “journal” file is created. That file is incorporated into the database when it is “committed”. If you don’t want to commit, you “rollback” the database to what it was before you made these latest changes. The temporary file contains the “transaction”, meaning the latest work you have just done to change the database. That is what the db1.Begin, db1.Commit and db1.Rollback mean.
The above program is a good template to adapt when making a database.
This application saves records of cash spending. You can allocate each expenditure to a category. Each time you allocate to a category, totals are worked out for the categories and you can see what fraction of your spending went to each of the categories.
If you know you spent, say, €100, and you can only account for, say €85, you can distribute the remaining €15 among the categories.
Before letting loose on the code and after a look at the form we shall take a look at the process of designing such an application.
The File menu has items MenuNewDatabase, MenuOpen and MenuQuit.
The Data Menu has items MenuNewSpending, MenuNewCategory, MenuClearSpending, MenuClearCategories, MenuRound, MenuUnselectAll, MenuCalculate and MenuCopy.
The Help menu is optional.
The textbox whose name you cannot quite see above is tbDistribute.
The program starts by opening the last database file that was open, or prompting to make a new one if it is the first time, or locating it if you sneakily moved it since the last time it was open. It also starts with a blank row in the Spending and Categories tableviews.
When a category is chosen for the selected spending line (click a category line anywhere except in the name column and press Enter) the category totals and percentages are recalculated.
Typing in the Target textbox is optional. If there is a number in it, “Still to do” will be calculated.
Internally, the database has two tables called Spending and Categories. You can see two tableviews corresponding to the two database tables. These are the fields in each table:
The two primary keys are SpendingID and CatID. They number the records in sequence (1, 2, 3...)
The Spending table’s Category field contains a number which, when you look it up in the Categories table, gives you the category name. This is good: if you change the spelling of a category name you only have to change it once.
The user does not need to see the record IDs. They are internal to the database. They have to be unique: each record must have its own record ID. They are the primary keys of the Spending and Categories tables. They will be the very first columns in the tableviews, but they will be hidden from view (zero width). Also, in the Spending table, the user does not want to see the Category ID (the reference to one of the categories). It will be the last column in the Spending table, and also zero width. The columns start from zero, so it is column 5, just to the right of the Amount column.
Having sketched out a form design and planned the tables and fields with pencil and paper, we next think of what we want the program to do. It is good to keep in mind the things databases do: Add, Delete, Modify (as well as display the data). Here is a list. These are going to be the subs.
Database
NewDatabase
Create a new database file on disk with its two tables
OpenDatabase
Open the database and display what is in it on startup
General
Calculate
Add up totals and work out percentages for each category
DoTotals
Grand totals for amounts in spending and categories tables
SetupTableViews
The right number of rows and columns and column headings
Spending Table
NewSpending
Add a record to the Spending table
ShowSpending
Display what is in the Spending table in tv1, the tableview
TidySpendingTable
The final part of ShowSpending, really. Alternating blue lines
SumSpending
Part of “DoTotals”; add up all the spending totals
Clear a category (make it a right-click menu)
Delete a record when you press DEL or BACKSPACE on a selected line
Categories Table
NewCategory
Add a record to the Categories table
ShowCategories
Display what is in the Categories table in tvCategories
TidyCategoriesTable
The final part of ShowCategories. Alternating blue lines.
SumCategories
Part of “DoTotals”; add up all the category amounts
Insert default categories into the categories table (a menu item)
EnterOnCategoryLine
Enter on a line inserts category on the selected spending line.
Delete a record when you press DEL or BACKSPACE on a selected line
Other Features
Work out how much is left to allocate
Distribute what is left among the categories
A Help window
Save what database we are using in Settings for next time
Copy everything as text, to paste into a word processing document
Round numbers to whole euros (and check totals are not out by one)
A Quit menu item to close the program
Useful Functions
CategoryNameFromID
Given the CatID number, return the Category Name (a string)
Massage
Given the user’s choice of filename, remove bad characters
Now it is time to program. Write the subs. Work out when they will be called on to do their work. Some can be consigned to menus. Some can happen when you click things. You are the one who is going to use this program: Do you want to click buttons? Do you want windows to pop up when you add a new category or a new spending transaction? Are there nice ways of doing things—intuitive ways—so things can happen naturally, as a new user might expect them to happen? We do some thinking and come up with some ideas:
We can do with a blank row in each table to start with, that you can type in.
When you finish typing in a cell, save that cell. Avoid having to click a Save button.
When you press Enter in the last cell of the line, make a new line.
When a category line is selected and the user presses Enter, put that category into whatever line in the spending table that is selected (highlighted). Move to the next spending line that doesn’t have a category so you can click a category line and Enter it. So you can enter categories for all the lines at the end, after you have entered everything else.
When you start, open the same database you had open last time. If none, give a choice of creating a new database or browsing to find the database that you moved or somebody may have given to you on a USB or in an email.
Edit a category by clicking on it.
Edit a cell in the spending table by clicking on it (except the category — just Enter on a line in the categories table to put a new one in.)
When you allocate a spending line to a category, recalculate the percentages for all categories.
When you change the total in the Target textbox, do a subtraction to find out how much you still have left to allocate.
Put blanks into cells that have nothing in them rather than zeros.
Pressing Delete or backspace in either of the tableviews will delete the selected (highlighted) line and delete its record from the database. No questions, no confirmation request—it just does it. Only one line can be deleted at a time, and it is easy enough to re-enter if you press Delete by mistake.
If the first cell on a tableview row has a record ID number in it, the record exists and saving just has to update it. If it is blank, the database has to first create a new record, giving it the next highest record number, put its record number in the first cell, and then update it.
Here are the names of the objects on the form FMain:
Panels: Panel1 (pink), Panel2 (blue)
Labels saying “Spending”, “Categories”, “Target:”, “= Done:”, “+ Still to do:”, “Amount:”
Labels called “LabSpendingTotal” and “LabCategoriesTotal” top right of the tableviews.
Help Menu: Help and Instructions (F1) (Opens a separate form called Help. Put on it what you like.)
Category Menu (invisible, so it is not on the main menubar): MenuClearCategory (This one pops up with you right-click a category cell in the spending table.)
Here is the code. Following it is an explanation of the SQL statements.
PublicfdbAsNewConnection'finance databasePublicrsAsResult'result set after querying databasePublicSQLAsStringPublicSubForm_Open()SetUpTableViewsIfIsNull(Settings["Database/host"])ThenSelectCaseMessage.Question("Create a new data file, or open an existing one?","New...","Open...","Quit")Case1'newNewDatabaseCase2'openOpenDatabase(Null,Null)CaseElseQuitEndSelectElseOpenDatabase(Settings["Database/host"],Settings["Database/name"])EndifEndPublicSubForm_Close()fdb.Close'close connectionEndPublicSubOpenDatabase(dbHostAsString,dbNameAsString)'if these are null, ask where the database isIfNotExist(dbHost&/dbName)OrIsNull(dbHost)Then'it's not where it was last time, or path not suppliedDialog.Title="Where is the database?"Dialog.Filter=["*.db"]Dialog.Path=User.Home&/"Documents/"IfDialog.OpenFile()ThenReturn' User pressed Cancel; still can't open a databaseDimsAsString=Dialog.PathDimpAsInteger=RInStr(s,"/")'position of last slashfdb.host=Left(s,p)fdb.Name=Mid(s,p+1)Elsefdb.host=dbHostfdb.Name=dbNameEndIfTryfdb.Closefdb.type="sqlite3"Tryfdb.OpenIffdb.OpenedThenFMain.Caption=fdb.host&/fdb.NameSettings["Database/host"]=fdb.hostSettings["Database/name"]=fdb.NameElseMessage.Info("<b>Couldn't connect.</b><br><br>... please try again or create a new database.")ReturnEndifShowSpendingShowCategoriesCalculateEndPublicSubNewDatabase()Dialog.Path=User.Home&"/"'setting it to "~/" doesn't workDialog.Title="Create a New Database"IfDialog.SaveFile()ThenReturn'clicked CancelDimsAsString=Dialog.Path&".db"DimpAsInteger=RInStr(s,"/")'position of last slashDimFNameAsString=Mid(s,p+1)fdb.host=Left(s,p)fdb.Name=""'This MUST be left blank. If not, database file will not be createdfdb.Type="sqlite3"IfExist(s)ThenKills'delete existing file of that namefdb.CloseTryfdb.Open'opens a connection to the database; do this after setting properties and before creatingIfErrorThenMessage("Unable to open the database file<br><br>"&Error.Text)ReturnEndiffdb.Databases.Add(fName)'does the creatingfdb.CloseDimdbTableAsTablefdb.name=fNameTryfdb.OpenIfNotfdb.openedThenMessage("Unable to open the data file")ReturnEndifdbTable=fdb.Tables.Add("Spending")dbTable.Fields.Add("SpendingID",db.Integer)dbTable.Fields.Add("TransDate",db.String)dbTable.Fields.Add("Category",db.Integer)dbTable.Fields.Add("Comment",db.String)dbTable.Fields.Add("Amount",db.Float)dbTable.PrimaryKey=["SpendingID"]dbTable.Updaters=fdb.Create("Spending")Iffdb.ErrorThenMessage("Couldn't create the Spending table.<br><br>: "&Error.Text)ReturnEndifrs!SpendingID=1rs!TransDate=""rs!Category=0rs!Comment=""rs!Amount=0.0rs.Updatefdb.CommitIffdb.ErrorThenMessage("Couldn't save a first record in the Spending table.<br><br>: "&Error.Text)ReturnEndiffdb.Closefdb.name=fNameTryfdb.OpenIfNotfdb.openedThenMessage("Unable to open the data file")ReturnEndifdbTable=fdb.Tables.Add("Categories")dbTable.Fields.Add("CatID",db.Integer)dbTable.Fields.Add("Category",db.String)dbTable.PrimaryKey=["CatID"]dbTable.Updaters=fdb.Create("Categories")Iffdb.ErrorThenMessage("Couldn't create the Categories table.<br><br>: "&Error.Text)ReturnEndifrs!CatID=1rs!Category=""rs.Updatefdb.CommitIffdb.ErrorThenMessage("Couldn't save a first record in the Categories table.<br><br>: "&Error.Text)ReturnEndifEndPublicSubDoTotals()labCategoriesTotal.Text=SumTheCategories()labSpendingTotal.text=SumTheSpending()tbDone.Text=labSpendingTotal.TextEndPublicSubShowSpending()rs=fdb.Exec("SELECT * FROM Spending")DimL,CatIDAsIntegerDimCatNameAsStringtv1.Rows.Count=0'clearIfNotIsNull(rs)ThenWhilers.Availabletv1.Rows.Count+=1L=tv1.Rows.maxtv1[L,0].text=rs!SpendingIDtv1[L,1].Text=rs!TransDatetv1[L,2].Text=Format(rs!Amount,"0.00")CatName=rs!CategoryIfNotIsNull(CatName)ThenCatID=If(IsNull(Val(CatName)),-1,Val(CatName))IfCatID>-1Thentv1[L,3].Text=CategoryNameFromID(CatID)Endiftv1[L,4].Text=rs!Commenttv1[L,5].Text=rs!Category'Category ID in this hidden columnrs.MoveNextWendEndifIftv1.Rows.Count=0Thentv1.Rows.Count=1TidySpendingTableEndPublicSubShowCategories()rs=fdb.Exec("SELECT * FROM Categories")DimLAsIntegerDimtAsFloattvCategories.Rows.Count=0'clearIfNotIsNull(rs)ThenWhilers.AvailabletvCategories.Rows.Count+=1L=tvCategories.Rows.maxtvCategories[L,0].text=rs!CatIDtvCategories[L,3].Text=rs!Categoryrs.MoveNextWendEndifIftvCategories.Rows.Count=0ThentvCategories.Rows.Count=1TidyCategoriesTableEndPublicSubNewSpending()tv1.Rows.count=tv1.Rows.count+1tv1.MoveTo(tv1.Rows.Max,1)tv1.EditEndPublicSubNewCategory()tvCategories.Rows.count=tvCategories.Rows.count+1tvCategories.row+=1tvCategories.EditEndPublicSubtv1_Insert()NewSpendingEndPublicSubtvCategories_Insert()NewCategoryEndPublicSubtv1_Click()SelectCasetv1.ColumnCase1,2,4tv1.EditCase3IftvCategories.Rows.Count>0ThentvCategories.SetFocustvCategories.Rows[0].Selected=TrueEndifEndSelectEndPublicSubtvCategories_Click()IftvCategories.Column=3ThentvCategories.EditEndPublicSubSetUpTableViews()DimiAsIntegertv1.Columns.count=6tv1.Rows.count=1tv1.Columns[0].Width=0tv1.Columns[1].Alignment=Align.Centertv1.Columns[2].Alignment=Align.RightFori=1Totv1.Columns.Max-1tv1.Columns[i].Width=Choose(i,80,80,130,tv1.Width-tv1.ClientW-306)tv1.Columns[i].Text=Choose(i,"Date","Amount","Category","Comment")NexttvCategories.Columns.count=4tvCategories.Rows.count=1tvCategories.Columns[0].Width=0Fori=1TotvCategories.Columns.MaxtvCategories.Columns[i].Width=Choose(i,60,60,tvCategories.Width-tvCategories.ClientW-350)tvCategories.Columns[i].Text=Choose(i,"Total","%","Category")NexttvCategories.Columns[1].Alignment=Align.righttvCategories.Columns[2].Alignment=Align.Centertv1.Columns[5].Width=0EndPublicSubTidySpendingTable()ForiAsInteger=0Totv1.Rows.MaxForjAsInteger=0Totv1.Columns.MaxIfj=2Orj=3Thentv1[i,j].Padding=4IfiMod2=1Thentv1[i,j].Background=&hF0F0FFNextNextEndPublicSubTidyCategoriesTable()ForiAsInteger=0TotvCategories.Rows.MaxForjAsInteger=1TotvCategories.Columns.MaxtvCategories[i,j].Padding=4IfiMod2=1ThentvCategories[i,j].Background=&hF0F0FFNextNextEndPublicSubMassage(sAsString)AsString'Doesn't like spaces or hyphens in file names. Doesn't complain; just doesn't create the file.DimzAsStringForiAsInteger=0ToLen(s)-1IfIsLetter(s[i])OrIsDigit(s[i])Ors[i]="_"Ors[i]="."Thenz&=s[i]Elsez&="_"NextReturnzEndPublicSubtvCategories_Save(RowAsInteger,ColumnAsInteger,ValueAsString)DimRecIDAsIntegerDimOriginalValueAsString=tvCategories[Row,Column].TexttvCategories[Row,Column].Text=ValueIfIsNull(tvCategories[Row,0].Text)Then'no record ID, so we need a new recordDimResAsResultSQL="SELECT MAX(CatID) as 'TheMax' FROM Categories"Res=fdb.Exec(SQL)IfIsNull(Res!TheMax)ThenRecID=1ElseRecID=Res!TheMax+1tvCategories[Row,0].Text=RecIDSQL="INSERT INTO Categories(CatID,Category) VALUES("&RecID&",'')"fdb.Exec(SQL)Iffdb.ErrorThenMessage("Couldn't save:<br><br>"&SQL&"<br><br>"&Error.Text)Endif'update the recordRecID=tvCategories[Row,0].TextSQL="UPDATE Categories SET Category = '"&Value&"' WHERE CatID='"&RecID&"'"Tryfdb.Exec(SQL)Iffdb.ErrorThenMessage("Couldn't save:"&SQL&"<br><br>"&Error.Text)IfValue<>OriginalValueThenShowSpending'category name was changedEndPublicSubtv1_Save(RowAsInteger,ColumnAsInteger,ValueAsString)DimRecIDAsIntegerDimFieldNameAsString=Choose(Column,"TransDate","Amount","Category","Comment")IfIsNull(tv1[Row,0].Text)Then'There's no Record ID, so insert a new recordDimResAsResultSQL="SELECT MAX(SpendingID) as 'TheMax' FROM Spending"TryRes=fdb.Exec(SQL)IfIsNull(Res!TheMax)ThenRecID=1ElseRecID=Res!TheMax+1tv1[Row,0].Text=RecIDSQL="INSERT INTO Spending(SpendingID,TransDate,Amount,Category,Comment) VALUES('"&RecID&"',' ',' ',' ',' ')"Tryfdb.Exec(SQL)IfErrorThenMessage("Couldn't save: "&Error.Text)ReturnEndifEndif'update recordRecID=tv1[Row,0].TextSQL="UPDATE Spending SET "&FieldName&" = '"&Value&"' WHERE SpendingID='"&RecID&"'"Tryfdb.Exec(SQL)IfErrorThenMessage("Couldn't save:"&SQL&"<br><br>"&Error.Text)ReturnEndifIfColumn=2Thentv1[Row,Column].Text=Format(Val(Value),"###0.00")Calculate'amount has changedElsetv1[Row,Column].Text=ValueEndifCatchMessage("Couldn't save ... have you created and opened a database yet?")StopEvent'Don't go automatically to the next cell. If you do, you'll get this message twice.EndPublicSubtv1_KeyPress()SelectCaseKey.CodeCaseKey.BackSpace,Key.Del'remove recordDimRecIDAsInteger=tv1[tv1.Row,0].TextSQL="DELETE FROM Spending WHERE SpendingID='"&RecID&"'"Tryfdb.Exec(SQL)IfErrorThenMessage("Couldn't delete<br><br>"&Error.Text)Elsetv1.Rows.Remove(tv1.Row)Iftv1.Rows.Count=0Thentv1.Rows.Count=1EndifCaseKey.Enter,Key.ReturnIftvCategories.Rows.Count>0ThentvCategories.SetFocustvCategories.Rows[0].Selected=TrueEndifEndSelectEndPublicSubtvCategories_KeyPress()SelectCaseKey.CodeCaseKey.BackSpace,Key.Del'remove recordDimRecIDAsInteger=tvCategories[tvCategories.Row,0].TextSQL="DELETE FROM Categories WHERE CatID='"&RecID&"'"Tryfdb.Exec(SQL)IfErrorThenMessage("Couldn't delete<br><br>"&Error.Text)ElsetvCategories.Rows.Remove(tvCategories.Row)EndifCaseKey.Enter,Key.ReturnEnterOnCategoryLine'action on pressing EntertvCategories.UnSelectAllEndSelectEndPublicSubMenuClearSpending_Click()fdb.Exec("DELETE FROM Spending")tv1.Rows.count=1tv1.ClearEndPublicSubMenuClearCategories_Click()fdb.Exec("DELETE FROM Categories")tvCategories.Rows.count=1tvCategories.ClearEndPublicSubCategoryNameFromID(IDAsInteger)AsStringDimresAsResult=fdb.Exec("SELECT Category FROM Categories WHERE CatID="&ID)IfNotres.AvailableThenReturn"?"IfIsNull(res!Category)ThenReturn"-"Returnres!CategoryEndPublicSubEnterOnCategoryLine()'apply this category to the selected Spending lineIftv1.row<0ThenReturnIfIsNull(tv1[tv1.row,0].text)ThenMessage("Please save this spending record first by entering some other item of data; there's no record ID yet.")ReturnEndiftv1[tv1.row,3].text=tvCategories[tvCategories.row,3].TextDimCategoryIDAsString=tvCategories[tvCategories.row,0].TextDimSpendingIDAsString=tv1[tv1.row,0].texttv1[tv1.row,5].text=CategoryIDSQL="UPDATE Spending SET Category='"&CategoryID&"' WHERE SpendingID='"&SpendingID&"'"Tryfdb.Exec(SQL)IfErrorThenMessage("Couldn't save the category<br><br>"&SQL&"<br><br>"&Error.text)ReturnEndifCalculateForiAsInteger=tv1.rowTotv1.Rows.MaxIfIsNull(tv1[i,3].text)Thentv1.Rows[i].Selected=True'select the next Spending row that needs a categorytvCategories.SetFocusReturnEndifNexttv1.SetFocusEndPublicSubCalculate()Dimi,j,CategoryIDAsIntegerDimt,GrandTotalAsFloatDimresAsResultDimsAsStringFori=0TotvCategories.Rows.Max'every categoryIfIsNull(tvCategories[i,0].Text)ThenContinueCategoryID=tvCategories[i,0].TextTryRes=fdb.Exec("SELECT Total(Amount) AS TotalAmount FROM Spending WHERE Category="&CategoryID)IfErrorThenMessage("Couldn't total<br><br>"&Error.Text)ContinueEndifWhileres.Availablet=res!TotalAmountGrandTotal+=tIft=0ThentvCategories[i,1].Text=""ElsetvCategories[i,1].Text=Format(t,"##0.00")res.MoveNextWendNextIfGrandTotal=0ThenReturnFori=0TotvCategories.Rows.Maxs=tvCategories[i,1].TextIfNotIsNull(s)AndIfVal(s)>0ThentvCategories[i,2].Text=Format(100*Val(s)/GrandTotal,"##0.##")ElsetvCategories[i,2].Text=""NexttbDone.Text=Format(GrandTotal,"##0.00")labSpendingTotal.Text=tbDone.TextlabCategoriesTotal.Text=SumTheCategories()IfNotIsNull(tbTarget.text)ThentbToDo.Text=Format(Val(tbTarget.Text)-GrandTotal,"##0.00")tbDistribute.Text=tbToDo.TextEndifEndPublicSubSaveCategoriesTable()ForiAsInteger=0TotvCategories.Rows.MaxSaveCategoryLine(i)NextEndPublicSubSaveCategoryLine(iAsInteger)'i is the line numberDimRecIDAsIntegerDimt,pctAsFloatDims,CategoryNameAsStringDimresAsResultRecID=Val(tvCategories[i,0].Text)CategoryName=tvCategories[i,3].Textt=If(IsNull(tvCategories[i,1].Text),0,Val(tvCategories[i,1].Text))s=tvCategories[i,2].Textpct=If(IsNull(s),0,Val(s))IfIsNull(RecID)Then'new record neededres=fdb.Exec("SELECT Max(CatID) AS MaxCatID FROM Categories")RecID=res!MaxCatID+1SQL="INSERT INTO Categories(CatID,Category) VALUES("&RecID&","&CategoryName&")"fdb.Exec(SQL)IfErrorThenMessage("Couldn't insert a new record<br><br>"&SQL&"<br><br>"&Error.text)ReturnEndifElseSQL="UPDATE Categories SET Category='"&CategoryName&"' WHERE CatID="&RecIDTryfdb.Exec(SQL)'before checking Error, don't forget to use TRY. Otherwise Error will be set and you'll seem to have an error when you don'tIfErrorThenMessage("Couldn't update a record<br><br>"&SQL&"<br><br>"&Error.text)ReturnEndifEndifEndPublicSubSumTheCategories()AsStringDimtAsFloatDimsAsStringForiAsInteger=0TotvCategories.Rows.Maxs=tvCategories[i,1].TextIfNotIsNull(s)Thent+=Val(s)NextReturnFormat(t,"##0.00")EndPublicSubSumTheSpending()AsStringDimtAsFloatDimsAsStringForiAsInteger=0Totv1.Rows.Maxs=tv1[i,2].TextIfNotIsNull(s)Thent+=Val(s)NextReturnFormat(t,"##0.00")EndPublicSubMenuCalculate_Click()CalculateEndPublicSubtbTarget_LostFocus()IfNotIsNull(tbTarget.text)ThentbTarget.Text=Format(Val(tbTarget.Text),"##0.00")ElsetbTarget.Text=""CalculateEndPublicSubtbTarget_KeyPress()IfKey.Code=Key.EnterOrKey.Code=Key.ReturnThenFMain.SetFocusEndPublicSubbDistribute_Click()Dimt,pct,y,zAsFloatIfIsNull(tbDistribute.Text)ThenReturnDimxAsFloat=Val(tbDistribute.Text)ForiAsInteger=0TotvCategories.Rows.MaxIfIsNull(tvCategories[i,1].Text)ThenContinueIfIsNull(tvCategories[i,2].Text)ThenContinuet=Val(tvCategories[i,1].Text)pct=Val(tvCategories[i,2].Text)y=t+pct/100*xz+=y'running totalIfy=0ThentvCategories[i,1].Text=""ElsetvCategories[i,1].Text=Format(y,"##0.00")SaveCategoryLine(i)NextlabCategoriesTotal.text=Format(z,"##0.00")FMain.SetFocusEndPublicSubtbDistribute_LostFocus()'when leaving, fix the appearanceIfNotIsNull(tbDistribute.text)ThentbDistribute.Text=Format(Val(tbDistribute.Text),"##0.00")ElsetbDistribute.Text=""EndPublicSubtbDistribute_KeyPress()'enter leaves the textboxIfKey.Code=Key.EnterOrKey.Code=Key.ReturnThenFMain.SetFocusEndPublicSubMenuDefaultCategories_Click()Tryfdb.Exec("DELETE FROM Categories")'it might be already clearedtvCategories.Rows.Count=9tvCategories.ClearDimsAsStringForiAsInteger=0To8s=Choose(i+1,"Provisions","Travel","Medical","Donations","Papers etc","Clothes","Personal","Phone","Repairs")tvCategories[i,3].text=stvCategories[i,0].text=i+1SQL="INSERT INTO Categories(CatID,Category) VALUES("&Str(i+1)&",'"&s&"')"Tryfdb.Exec(SQL)IfErrorThenMessage("Couldn't insert a new record in the categories table.<br><br>"&SQL&"<br><br>"&Error.Text)NextlabCategoriesTotal.text=""EndPublicSubMenuRound_Click()DimsAsStringDimx,tAsFloatForiAsInteger=0TotvCategories.Rows.Maxs=tvCategories[i,1].TextIfIsNull(s)ThentvCategories[i,1].Text=""Elsex=Round(Val(s))t=t+xtvCategories[i,1].Text=xEndifNextlabCategoriesTotal.Text=Format(t,"##0.00")ForiAsInteger=0TotvCategories.Rows.Maxs=tvCategories[i,2].TextIfNotIsNull(s)ThentvCategories[i,2].Text=Round(Val(s))NextEndPublicSubMenuOpen_Click()OpenDatabase(Null,Null)EndPublicSubMenuNewDatabase_Click()NewDatabaseEndPublicSubMenuNewSpending_Click()NewSpendingEndPublicSubMenuNewCategory_Click()NewCategoryEndPublicSubMenuQuit_Click()QuitEndPublicSubMenuCopy_Click()Dims,zAsStringDimi,jAsIntegerFori=0Totv1.Rows.Maxs=tv1[i,1].TextForj=2To4s&=gb.Tab&tv1[i,j].TextNextz&=If(IsNull(z),"",gb.NewLine)&sNextz&=gb.NewLineFori=0TotvCategories.Rows.Maxs=tvCategories[i,1].TextForj=2To3s&=gb.Tab&tvCategories[i,j].TextNextz&=If(IsNull(z),"",gb.NewLine)&sNextz&=gb.NewLine&gb.NewLine&"Total Withdrawn: "&tbTarget.Text&gb.tab&" = Total Accounted For: "&tbDone.Text&gb.tab&" + Cash on hand: "&tbToDo.TextClipboard.Copy(z)EndPublicSubMenuShowHelp_Click()Help.ShowModalEndPublicSubMenuClearCategory_Click()DimRecIDAsInteger=tv1[tv1.row,0].Textfdb.Exec("UPDATE Spending Set Category=' ' WHERE SpendingID="&RecID)tv1[tv1.row,3].Text=""'Cat texttv1[tv1.row,5].Text=""'Cat IDCalculateEndPublicSubMenuUnselectAll_Click()tv1.Rows.UnSelectAlltvCategories.Rows.UnSelectAllEnd
Some of these statements are used as they appear. Others are a string that is built up from parts. You might see SQL = … . Bits of the statement are SQL and the field name might be added to it in the right place and be stored in a variable, for example. Or perhaps the record ID might be in a variable called RecID. Use single quotes in the string that is sent to SQLite. Use double quotes when assembling the statement in Gambas.
SELECT * FROM Spending
Select everything from the Spending table
SELECT * FROM Categories
Select everything from the Categories table
SELECT MAX(CatID) as 'TheMax' FROM Categories
Get the highest CatID from the Categories table and call it “TheMax”.
INSERT INTO Categories(CatID,Category) VALUES(123,'Entertainment')
Create a new record in the Categories table.
Put 123 into the CatID field and Entertainment into the Category field.
UPDATE Categories SET Category = 'Entertainment' WHERE CatID='123'
The Categories table has to be updated.
In the record with CatID equal to 123, put Entertainment in the Category field.
SELECT MAX(SpendingID) as 'TheMax' FROM Spending
Find the biggest SpendingID in the Spending table and call it “TheMax”.
INSERT INTO Spending(SpendingID,TransDate,Amount,Category,Comment) VALUES('123',' ',' ',' ',' ')
Create a new record in the Spending table.
SpendingID = 123
TransDate = a blank
Amount = a blank
Category = a blank
Comment = a blank
UPDATE Spending SET TransDate = '4-11-2019' WHERE SpendingID='123'
Put “4-11-2019” into the TransDate field of the record in the Spending table that has a SpendingID of 123.
DELETE FROM Spending WHERE SpendingID='123'
Delete the record in the Spending table that has a record ID of 123.
DELETE FROM Categories WHERE CatID='123'
Delete the record in the Categories table that has a record ID of 123.
DELETE FROM Spending
Delete every record from the Spending table. All the data disappears, never to be seen again.
DELETE FROM Categories
Delete every record from the Categories table. All the category records, gone forever.
SELECT Category FROM Categories WHERE CatID=123
Give me the name of the category that goes with the CatID record number 123.
UPDATE Spending SET Category='4' WHERE SpendingID='123'
Set the Category field of record 123 of the Spending table to 4. This spending item goes into the fourth category, whatever that is. To find out what the fourth category is, look up the Categories table and find the record with CatID=4
SELECT Total(Amount) AS TotalAmount FROM Spending WHERE Category='4'
Get the sum of all the numbers in the Amount fields of all the records in the Spending table that have 4 in their Category field. Simply, add up all the amounts spent in category 4. Call the answer “TotalAmount”
SELECT Max(CatID) AS MaxCatID FROM Categories
Get the highest CatID from the Categories table. Call it MaxCatID.
SQL = "INSERT INTO Categories(CatID,Category) VALUES(4,Travel)"
Create a new Categories record. Set the CatID field equal to 4 and the Category to “Travel”.
UPDATE Categories SET Category='Travel' WHERE CatID=4
Update the Categories record that has a record ID of 4. Put “Travel” into the Category field.
UPDATE Spending Set Category=' ' WHERE SpendingID=123
Put a blank into the Category field of Spending record 123
The statements are either SELECT, INSERT, DELETE or UPDATE.
A most important point about using the UPDATE statement:
Be careful when updating records.
If you omit the WHERE clause, ALL records will be updated!
For example, do not write this: UPDATE Spending SET Amount=12.50 .This puts 12.50 into the Amount field of every record. All amounts become 12.50. You should say UPDATE Spending SET Amount=12.50 WHERE SpendingID=42 .
When practising printing, printing “to a file” will save paper. You can open the resulting PDF (Portable Document Format) file in your favourite PDF reader, such as Okular, and see on screen what you would get on paper.
This is about the simplest demonstration of printing. In your program you need a “printer”. We have used objects like buttons and tableviews. You can see them. A printer, though, is invisible. There is a printer class, just as there is a button class. You drag a printer onto your form just the same as you would drag a button or any other object. On the form it looks like a printer, but when the program runs it cannot be seen. It is really just a lump of code that is built into Gambas and does the things that printers are supposed to do, namely print and look after page sizes and orientation and so on. Printer is a clever little object.
First you tell your Printer object to configure, then you tell it to print. (“Printer, print!”, or as we write it in Gambas, prn1.print ). When you tell it to print it will issue the Draw event. In the draw event you put things on the page that you want printed. You do this with all the abilities that another class has, the Paint class. The Paint class can put things onto the page for printing, but it has other uses too, such as painting into DrawingAreas or ScrollAreas on the form. Right: here we go!
This small form has a Printer object and a button called bPlainPrint.
PublicSimpleTextAsStringPublicSubForm_Open()SimpleText="Countries of the World<br><br>Papua New Guinea<br><br>Papua New Guinea is a country that is north of Australia. It has much green rainforest. It has beautiful blue seas. Its capital, located along its southeastern coast, is Port Moresby.<br><br>This is plain text in Linux Libertine 12 point.<br><br>John Smith, editor"EndPublicSubpr1_Draw()Paint.Font=Font["Linux Libertine,12"]Paint.DrawRichText(SimpleText,960,960,Paint.Width-2*960)EndPublicSubbPrintPlain_Click()Ifpr1.Configure()ThenReturn'if user clicks Cancel, don't continuepr1.PrintEnd
When the form opens, some text is put in a variable called SimpleText for printing.
When the button is clicked the printer pr1 is told to configure itself. If the user clicks the Cancel button this returns the value of True, so we should do nothing more. Otherwise, dear friendly printer object, please print.
The printer object pr1 sends us the Draw event. It is saying, “I want to draw something! Please tell me what to paint on the page!”. We oblige by saying
Paint.Font is a property describing the font. It is a property with parts to it. We assemble those parts using Font[something]. The something is a string. For example, Font["Bitstream Vera Serif,Bold,24"] means “assemble a font that is Bitstream Vera Serif, bold, 24 point”. That is put in the Font property of the Paint thing. Actually the Paint thing is just a collection of skills. It is nothing you can see. It is another invisible class. Be careful not to put spaces in that string unless part of the font name. Gambas Help warns you of this. No spaces either side of the commas!
Paint.DrawRichText(something) is one of paint’s skills. It is a method it knows how to do. It needs at least three things in brackets. It can take a few more. Here we have four “arguments”, or “things in brackets”. First item: what to print. Second item: how far across to begin printing. Third item: how far down to begin printing. The 960 will give an inch margin. 96 dots per inch is a typical default printer resolution. The number is “tenths of a dot”. (I hope I have that right.) Fourth item: how wide is my printing going to be? Answer: the full width that Paint will allow less an inch on the left and an inch on the right. Each inch is 960. Take away two of them.
<br> means “break”, which goes to the next line. <br><br> means go to a new line, then go to a new line again. It gives us a blank line.
Rich Text understands <br>. It also understands quite a few other symbols planted in the text. There are symbols to make it print using “Heading 1” style, and “Heading 2” and so on. You cannot change what these styles look like, though. They are built in and that is that. You can also change fonts and print size and colour anywhere in your text. These codes make the print come out a certain way. In fact, it is a language in itself: HyperText Markup Language, or HTML. For example, to switch on Bold, put in this tag: <b>. When you want to switch Bold off, put in this one: </b>.
FancyText = "<h3>Countries of the World</h3><Small>Papua New Guinea</Small><hr><br>Papua New Guinea is a country that is north of <font color = #780373><b>Australia</b>.</font><font color = black> It has much</font><font color = green> green rainforest</font><font color = black>. It has beautiful <font color = blue>blue seas</font><font color = black>. Its capital, located along its southeastern coast, is <Font Face=Times New Roman, Color=#FF0000>Port Moresby</font>.<br><br>This is written in <font face = Arial>HTML.<br></font><p align = right><font face = Times New Roman, Size=+1><b>John Smith</b></font>, <i>editor</i></p>"
Incidentally, that text, if saved in a text file with the extension .html, will open and display in a web browser, such as FireFox. You can try it.
The result will be:
I used Heading 3 ( <h3> … </h3> ) because Heading 1 was gross.
There are many tags in the text to make it look like that. Gambas allows these tags. It is only a small selection from the full HTML. Save a document in HTML in your word processor, open it in a text editor like Kate, and be amazed.
Drag a picture file onto the Data folder. Set the Picture property of the PictureBox to it.
The Printer is named pr1.
PublicPicAsPicturePublicSubpr1_Draw()Paint.DrawPicture(Pic,960,960,3000,3000)EndPublicSubbPrint_Click()Ifpr1.Configure()ThenReturn'if user clicks Cancel, don't continuePic=PictureBox1.Picturepr1.PrintEnd
The picture is scaled to be 3000 x 3000. When I print to a file, the resolution is 96 dots per inch (96 dpi). The picture is printed 1 inch from the top and left margins and is scaled to fit into about 3 inches x 3 inches (3000x3000).
In this program, 40 names are invented and put in an array called Z[]. If you were serious, the list of names could be typed in by the user or loaded from a names file or obtained from a database.
The names are printed down the page. There needs to be a side margin, and here it is set to an inch (960 dots when printing to PDF). It is stored in the variable (private property of the form) SideMargin. It is the same on the left and the right. The top margin is TopMargin.
When you print a name, how far down do you go before printing the next? LineSpacing is set to 280. That works out at about 0.3 of an inch. (960 is an inch).
The plan is: Print a name. However long that name is, move along a bit. That is the starting point for a horizontal line. Line as far as the page width less the right side margin. Draw the line. Go down a linespacing. Print the next name. Draw its line. Go down. Print a name. Draw its line, and so on.
Then draw the vertical lines to make the boxes. Start a little to the right of the width of the longest name. Step 330 dots, draw a vertical line, step another 330 dots, draw the next line, and so on. Don’t go past the end point of the horizontal lines. Finally, to make the right hand edge neat, draw a final vertical line. The Printer is called Prn. The button is bPrint.
PrivatezAsNewString[]PrivateLineSpacingAsInteger=280PrivateTopMarginAsInteger=960PrivateSideMarginAsInteger=960PublicSubPrn_Draw()DimsAsStringDimiAsIntegerDimNameWidth,HowFarDown,MaxNameWidth,MaxDistanceDownAsFloatDimMaxWidthAsFloat=Paint.Width-2*SideMarginPaint.Font=Font["Linux Libertine,12"]Paint.Color(Color.Black)Paint.MoveTo(SideMargin,TopMargin)'start herePaint.LineTo(Paint.Width-SideMargin,TopMargin)'draw to herePaint.Stroke'paint the top lineFori=0Toz.Maxs=z[i]NameWidth=Paint.TextExtents(s).Width+180'gap at the end about 1/5 inchMaxNameWidth=Max(MaxNameWidth,NameWidth)'remember the width of the longest nameHowFarDown=TopMargin+(LineSpacing*(i+1))Paint.DrawText(s,SideMargin,HowFarDown)Paint.MoveTo(SideMargin+NameWidth,HowFarDown)'starting positionPaint.LineTo(Paint.Width-SideMargin,HowFarDown)'finishing positionPaint.Stroke'draw the lineNextMaxDistanceDown=TopMargin+z.Count*LineSpacing'vertical lines go down to hereFori=SideMargin+MaxNameWidth+100ToPaint.Width-SideMarginStep330'step across the page every 1/3 inchPaint.MoveTo(i,TopMargin)Paint.LineTo(i,MaxDistanceDown)Paint.StrokeNextPaint.MoveTo(Paint.Width-SideMargin,TopMargin)Paint.LineTo(Paint.Width-SideMargin,MaxDistanceDown)Paint.Stroke'final line on rightEndPublicSubGetNames()DimFNAsString[]=["Oliver","Jack","Harry","Jacob","Charlie","Thomas","George","Oscar","James","William","Amelia","Olivia","Isla","Emily","Poppy","Ava","Isabella","Jessica","Lily","Sophie"]DimSNAsString[]=["Smith","Jones","Williams","Brown","Wilson","Taylor","Moreton","White","Martin","Anderson","Johnson","Walsh","Miller","Davis","Burns","Murphy","Lee","Roberts","Singh","Evans"]FN.ShuffleSN.ShuffleDimi,nAsIntegerFori=1To40z.Add(FN[n]&" "&SN[n])n+=1Ifn>FN.MaxThenFN.ShuffleSN.Shufflen=0EndifNextEndPublicSubbPrint_Click()Prn.OutputFile=User.Home&/"Names.pdf"'I'm printing to a pdf fileIfPrn.Configure()ThenReturnGetNamesPrn.Print()End
The form contains PictureBox1, a printer called Prn, and two buttons called bPicture and bPrint.
This program prints a calendar for the current month. When you look at the page you want to print you will see the “things” that have to be printed in various places. There are three things that call for repetition: the boxes, the numbers in the top left corner of each, and the names of the days of the week.
I gave PictureBox1 a default picture (that shows as soon as the program is run). First I dragged a photo onto the Data folder. Then I set the Picture property of the picturebox to it.
If you do not have a picture to begin with, the user needs to click the Choose Picture... button before clicking Print. The picture is stored in a property called Pic. If it is null printing does not proceed.
PublicPicAsPicturePublicSubLoadPicture()DimPathAsStringDialog.Title="Please Select a picture"Dialog.Filter=["*.jpg","*.png","Image Files","*","All files"]Dialog.Path=User.HomeIfDialog.OpenFile()ThenReturnPic=Picture.Load(Dialog.Path)PictureBox1.Picture=PicEndPublicSubbPicture_Click()LoadPictureFMain.SetFocusEndPublicSubbPrint_Click()Pic=PictureBox1.Picture'This line can be deleted if you don't give your PictureBox a default picture.IfIsNull(Pic)ThenMessage("Please select a photo first.")ElsePrn.OutputFile=User.Home&/"Calendar.pdf"IfPrn.Configure()ThenReturnPrn.PrintEndifEndPublicSubPrn_Draw()DimLeftMarginAsFloat=480'half inchDimTopMarginAsFloat=1200'inch and a bitDimrow,col,DayNum,CellNumAsIntegerDimsAsStringDimThisMonthAsInteger=Month(Date(Now))'the month number of the date part of the NOW function; 1 to 12DimThisYearAsInteger=Year(Date(Now))'current yearDimFirstOfMonthAsDate=Date(ThisYear,ThisMonth,1)DimStartDayAsInteger=WeekDay(Date(FirstOfMonth))'the weekday of the first of the monthDimTextHeight,TextWidth,GridTopAsFloatGridTop=7.2*960'Big PhotoPaint.DrawPicture(Pic,LeftMargin,TopMargin/2,Paint.Width-2*LeftMargin,5*960)'5 inch height'Month and Year titlePaint.Font=Font["Copperplate33bc,32"]TextHeight=Paint.TextExtents("S").Height'the height of a characters=Choose(ThisMonth,"January","February","March","April","May","June","July","August","September","October","November","December")&" "&ThisYearPaint.DrawText(s,0,GridTop-1000,Paint.Width,TextHeight,Align.Center)'inch above grid top'GridDimSideAsFloat=(Paint.Width-2*LeftMargin)/7'one-seventh of the width between marginsForrow=0To4Forcol=0To6Paint.DrawRect(LeftMargin+Side*Col,GridTop+Side*Row,Side,Side,Color.Black)'each squareNextNext'Days of the Week headingsPaint.Font=Font["Apple Chancery,12"]TextHeight=Paint.TextExtents("S").Height'the height of a characterForcol=0To7s=Choose(col+1,"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday")Paint.DrawText(s,LeftMargin+Side*Col,GridTop-TextHeight-96,Side,TextHeight,Align.Center)Next'DatesDimDaysInMonthAsIntegerIfThisYearMod4=0AndThisMonth=2ThenDaysInMonth=29ElseDaysInMonth=Choose(ThisMonth,31,28,31,30,31,30,31,31,30,31,30,31)Paint.Font=Font["Linux Libertine,20"]TextHeight=Paint.TextExtents("1").Height'the height of a digitForrow=0To4Forcol=0To6CellNum=7*row+colIfCellNum>=StartDayThenDayNum+=1IfDayNum>DaysInMonthThenReturn'Don't go to 35 days in the month!s=If(Col=0,"<font color=#DD0000>"&DayNum&"</Font>","<font color=#000000>"&DayNum&"</Font>")Paint.DrawRichText(s,LeftMargin+Side*Col+96,GridTop+Side*Row+TextHeight+96)EndifNextNextRow=0Col=0WhileDayNum<DaysInMonth'Put extra dates up in the top left of the grid.DayNum+=1s=If(Col=0,"<font color=#DD0000>"&DayNum&"</Font>","<font color=#000000>"&DayNum&"</Font>")Paint.DrawRichText(s,LeftMargin+Side*Col+96,GridTop+Side*Row+TextHeight+96)Col+=1'next columnWendEnd
The idea for this program is simple: whatever text you have on the clipboard, click on a handy little tray icon and it will be saved to an SQLite database. Nothing seems to happen, but the text is saved in a new record.
There is also the means to search for notes already saved. This is done in a window that appears when you middle-click the tray icon. I would have preferred it to appear on right-clicking the icon, but right-clicking can only make a menu appear. That is what right-clicks do: they show contextual menus. Now, middle-clicking is fine if you have a mouse with a middle wheel or button that can be clicked. For those of us who use a laptop’s trackpad, middle-clicking is simulated by clicking both left and right buttons simultaneously. On KDE there is an option in System Settings that can disable this, but by default it is enabled.
Experiment with the main window being hidden when the application starts if you like, or not the skiptaskbar property so the minimised main window does not get listed with the other open applications in the panel, but the effects did not appeal at all so they have been left at their defaults.
A Google image search will find a suitable picture for the tray icon:
It does not need to be any particular size. It scales itself nicely when the program runs.
The window has a large text area to show a note, and a textbox at the top left in which to type search text. There are also three labels that display the date/time when the note was saved, the position of the note among all the notes that have been found with that search text in them, and, for interest, the record ID of the note.
The tray icon (bottom left corner) can be placed anywhere. It does not appear in the window. It appears in the system tray (usually at the bottom right corner of the screen, in KDE’s default panel).
The tray icon comes in its own component, so check Project > Properties to see that it and the database components are included:
In summary, to use this notebook:
Copy any text to the clipboard.
Click the tray icon and your text is saved.
Click the tray icon with both left and right mouse buttons at the same time to search for a note.
Sometimes it is useful to save the text then bring up the window and add some key words that will help you find the note again. I sometimes add the words JOKE or QUOTE or FAMILY HISTORY. That way, by typing “QUOTE” in the search box, all my quotes appear and I can step through them one at a time. Copying all selected notes would be a useful feature to add.
The SQL select statement appears in the window caption, for interest.
For good measure, a TEXT menu has four entries for adjusting text:
Type Extra… positions the cursor at the end of the text of the visible note, ready for you to type something extra.
Tidy gets rid of multiple spaces, multiple tabs and multiple blank lines, and removes leading and trailing spaces.
Sentences joins broken sentences. Text copied from emails often has distinct lines.
Double-space separates the paragraphs with a blank line.
These text operations work on the whole of the text if there is no selection, or on the selected text if there is a selection. The shortcuts are there because often I find myself doing the last three in quick succession to get the text looking decent.
The other menus are:
The database is an SQLite file. You might wonder where the OPEN and NEW menu items are. For simplicity, the program creates a new database when it opens if none exists with the name and in the place it expects to find it. It is called Notebook.sqlite and it is in the Home directory. It is easy enough to change the code to make it in the Documents directory if you wish.
File > Backup is a useful menuitem that copies the database file to a Backups folder in the Documents folder, date-stamping the file name to show when it was created and to not interfere with earlier backups.
File > Renumber makes the record ID’s sequential, with no gaps. It, and the Vacuum item below it, are unnecessary. Vacuum tidies the internal structure of the database file. It uses SQLite’s inbuilt vacuum command.
File > Quit closes the application. File > Hide makes the window invisible. Clicking the close box on the window also only hides the window; it does not close the application. For this trick, the window’s Persistent property is set to true. The window persists, invisibly, when its close box is clicked.
When you type in the search textbox an SQL Select statement selects all the notes that contain that text. It is a simple search: it does not search for notes containing any of the words, ordered from best to least matching. It searches for exactly the text that is typed. As you type each letter the search is performed. A public property in the mdb module, Public rs As Result, stores the results of the search. Notes > Show All clears the search and finds all the notes. This happens when the program starts: all notes are selected.
Notes > New… lets you type a new note that is saved when you leave the text area.
Notes > Delete deletes the note currently displayed from the database.
Notes > Clear All clears all notes from the database. There is no undo but there is a chance to bail.
Window properties are:
Arrangement = Vertical
Stacking = Above
Width = 800
Height = 500
The Tray Icon (ti1) properties that need to be set are:
Tooltip = Click to save clipboard text
Visible = True
Be sure to set it to visible or you will not see the tray icon when the program is run, and there will be no elegant way of quitting the program when it is running after you hide the window that shows initially.
The Stacking property ensures that the window remains above other windows. If you click in the window belonging to your web browser, for example, the Notebook window does not get covered by the browser window but remains on top, floating above it. This can be useful for utility type programs.
The last thing to note before getting to the code is the use of the keyboard’s arrow keys. UP and DOWN take you to the first and last of the found notes respectively. LEFT and RIGHT step you back or forwards through the found notes. “Found notes” means those that are found to have the string of text in them that you typed in the textbox, or, if nothing has been typed, all the notes.
A few comments follow the code.
Code relating to the database is collected in a module called mdb.
' Gambas module filePublicdb1AsNewConnectionPublicrsAsResultPublicSQLSelAsStringPublicSubCreateDatabase()db1.Type="sqlite"db1.host=User.homedb1.name=""'delete an existing Notebook.sqliteIfExist(User.home&"/Notebook.sqlite")ThenKillUser.home&"/Notebook.sqlite"Endif'create Notebook.sqlitedb1.Opendb1.Databases.Add("Notebook.sqlite")db1.CloseEndPublicSubConnectDatabase()db1.Type="sqlite"db1.host=User.homedb1.name="Notebook.sqlite"db1.OpenSelectAllNotesDebug("Notes file connected:<br>"&db1.Host&/db1.Name&"<br><br>"&rs.Count&" records")EndPublicSubMakeTable()DimhTableAsTabledb1.name="Notebook.sqlite"db1.OpenhTable=db1.Tables.Add("Notes")hTable.Fields.Add("KeyID",db.Integer)hTable.Fields.Add("Created",db.Date)hTable.Fields.Add("Note",db.String)hTable.PrimaryKey=["KeyID"]hTable.UpdateMessage("Notes file created:<br>"&db1.Host&/db1.Name)EndPublicSubSelectAllNotes()rs=db1.Exec("SELECT * FROM Notes")FMain.Caption="SELECT * FROM Notes"rs.MoveLastEndPublicSubMassage(zAsString)AsStringWhileInStr(z,"''")>0'this avoids a build-up of single apostrophesReplace(z,"''","'")WendReturnReplace(z,"'","''")EndPublicSubAddRecord(sAsString,tAsDate)AsStringDimrs1AsResultDimNextIDAsIntegerIfrs.Max=-1ThenNextID=1ElseNextID=db1.Exec("SELECT Max(KeyID) AS TheMax FROM Notes")!TheMax+1db1.Beginrs1=db1.Create("Notes")rs1!KeyID=NextIDrs1!Created=t'timers1!Note=Massage(s)rs1.Updatedb1.CommitSelectAllNotesReturnNextIDCatchdb1.RollbackMessage.Error(Error.Text)EndPublicSubUpdateRecord(RecNumAsInteger,NewTextAsString)db1.Exec("UPDATE Notes SET Note='"&Massage(NewText)&"' WHERE KeyID="&RecNum)DimposAsInteger=rs.Index'Refresh the result cursor, so the text in it is updated as well as in the database file. This is tricky.IfIsNull(SQLSel)Thenrs=db.Exec("SELECT * FROM Notes")Elsers=db.Exec(SQLSel)'SQLSel is the last search, set by typing in tbSearchrs.MoveTo(pos)'Ooooh yes! It did it.CatchMessage.Error("<b>Update error.</b><br><br>"&Error.Text)EndPublicSubMoveRecord(KeyCodeAsInteger)AsBooleanIfrs.Count=0ThenReturnFalseSelectCaseKeyCodeCaseKey.LeftIfrs.Index>0Thenrs.MovePreviousElsers.MoveLastReturnTrueCaseKey.RightIfrs.Index<rs.MaxThenrs.MoveNextElsers.MoveFirstReturnTrueCaseKey.Uprs.MoveFirstReturnTrueCaseKey.Downrs.MoveLastReturnTrueEndSelectReturnFalseEndPublicSubClearAll()IfMessage.Warning("Delete all notes? This cannot be undone.","Ok","Cancel")=1Thendb1.Exec("DELETE FROM Notes")SelectAllNotesEndifEndPublicSubDeleteRecord(RecNumAsInteger)db1.Exec("DELETE FROM Notes WHERE KeyID='"&RecNum&"'")SelectAllNotesEndPublicSubSearchFor(sAsString)SQLSel="SELECT * FROM Notes WHERE Note LIKE '%"&Massage(s)&"%'"IfIsNull(s)ThenSelectAllNotesSQLSel=""ElseFMain.Caption=SQLSelrs=db1.Exec(SQLSel)EndifEndPublicSubRenumber()DimresAsResult=db.Exec("SELECT * FROM Notes ORDER BY KeyID")DimiAsInteger=1DimxAsIntegerApplication.Busy+=1Whileres.Availablex=res!KeyIDdb.Exec("UPDATE Notes SET KeyID="&i&" WHERE KeyID="&x)i+=1res.MoveNextWendSelectAllNotesApplication.Busy-=1EndPublicSubVacuum()AsStringDimfSize1,fSize2AsFloatfSize1=Stat(db1.Host&/db1.Name).Size/1000'kBdb1.Exec("Vacuum")fSize2=Stat(db1.Host&/db1.Name).Size/1000'kBDimUnitsAsString="kB"IffSize1>1000Then'megabyte rangefSize1/=1000fSize2/=1000Units="MB"EndifReturnFormat(fSize1,"#.0")&Units&" -> "&Format(fSize2,"#.0")&Units&" ("&Format(fSize1-fSize2,"#.00")&Units&")"EndPublicSubBackup()AsStringIfNotExist(User.Home&/"Documents/Backups/")ThenMkdirUser.Home&/"Documents/Backups"DimfnAsString="Notebook "&Format(Now,"yyyy-mm-dd hh-nn")DimsourceAsString=db1.Host&/db1.NameDimdestAsString=User.Home&/"Documents/Backups/"&fnTryCopysourceTodestIfErrorThenReturn"Couldn't save -> "&Error.TextElseReturn"Saved -> /Documents/Backups/"&fnEnd
' Gambas class filePublicOriginalTextAsStringPublicSubti1_Click()DimTimeAddedAsStringIfClipboard.Type=Clipboard.TextThenTimeAdded=mdb.AddRecord(Clipboard.Paste("text/plain"),Now())EndPublicSubForm_Open()IfNotExist(User.Home&/"Notebook.sqlite")Then'create notebook data filemdb.CreateDatabasemdb.MakeTablemdb.SelectAllNotesElsemdb.ConnectDatabaseShowRecordEndifEndPublicSubMenuQuit_Click()mdb.db1.Closeti1.DeleteQuitEndPublicSubForm_KeyPress()Ifmdb.MoveRecord(Key.Code)ThenShowRecordStopEventEndifEndPublicSubShowRecord()Ifmdb.rs.count=0ThenClearFieldsReturnEndifta1.Text=Replace(mdb.rs!Note,"''","'")DimdAsDate=mdb.rs!CreatedlabTime.text=Format(d,gb.MediumDate)&" "&Format(d,gb.LongTime)labRecID.text=mdb.rs!KeyIDlabLocation.text=Str(mdb.rs.Index+1)&"/"&mdb.rs.CountOriginalText=ta1.TextEndPublicSubMenuClear_Click()mdb.ClearAllClearFieldslabTime.Text="No records"EndPublicSubMenuCopy_Click()Clipboard.Copy(ta1.Text)EndPublicSubMenuDeleteNote_Click()DimRecNumAsInteger=Val(labRecID.Text)mdb.DeleteRecord(RecNum)'after which all records selected; now to relocate...DimresAsResult=db.Exec("SELECT * FROM Notes WHERE KeyID<"&RecNum)res.MoveLastDimiAsInteger=res.Indexmdb.rs.MoveTo(i)ShowRecordEndPublicSubClearFields()ta1.Text=""labRecID.Text=""labTime.Text=""labLocation.Text=""EndPublicSubMenuNewNote_Click()ClearFieldsta1.SetFocusEndPublicSubta1_GotFocus()OriginalText=ta1.TextEndPublicSubta1_LostFocus()Ifta1.Text=OriginalTextThenReturn'no changeSaveOrUpdateEndPublicSubtbSearch_Change()mdb.SearchFor(tbSearch.Text)ShowRecordCatchMessage.Error(Error.Text)EndPublicSubta1_KeyPress()IfKey.Code=Key.EscThenMe.SetFocus'clear focus from textarea; this triggers a record updateEndPublicSubtbSearch_KeyPress()IfKey.Code=Key.EscThenMe.SetFocusEndPublicSubMenuShowAll_Click()mdb.SelectAllNotesShowRecordEndPublicSubKeepReplacing(InThisAsString,LookForAsString,BecomesAsString)AsStringDimzAsString=InThisWhileInStr(z,LookFor)>0z=Replace(z,LookFor,Becomes)WendReturnzEndPublicSubSaveOrUpdate()IfIsNull(labRecID.Text)Then'new recordIfIsNull(ta1.Text)ThenReturnDimdAsDate=Now()labRecID.Text=mdb.AddRecord(ta1.Text,d)labTime.text=Format(d,gb.MediumDate)&" "&Format(d,gb.LongTime)Else'updateIfIsNull(ta1.Text)Thenmdb.DeleteRecord(Val(labRecID.Text))ClearFields'maybe leave everything empty?Elsemdb.UpdateRecord(Val(labRecID.Text),ta1.Text)EndifEndifEndPublicSubMenuTidy_Click()OriginalText=ta1.TextIfIsNull(ta1.Text)ThenReturnDimzAsString=If(ta1.Selection.Length=0,Trim(ta1.Text),Trim(ta1.Selection.Text))z=KeepReplacing(z,gb.NewLine,"|")z=KeepReplacing(z,gb.Tab&gb.Tab,gb.Tab)z=KeepReplacing(z," "," ")z=KeepReplacing(z,"| ","|")z=KeepReplacing(z,"|"&gb.tab,"|")z=KeepReplacing(z,"||","|")z=KeepReplacing(z,"|",gb.NewLine)Ifta1.Selection.Length=0Thenta1.Text=zElseta1.Selection.Text=zSaveOrUpdateEndPublicSubMenuSentences_Click()OriginalText=ta1.TextIfIsNull(ta1.Text)ThenReturnDimzAsString=If(ta1.Selection.Length=0,Trim(ta1.Text),Trim(ta1.Selection.Text))z=KeepReplacing(z,gb.NewLine,"~")z=KeepReplacing(z,"~ ","~")z=KeepReplacing(z,".~","|")z=KeepReplacing(z,"~"," ")z=KeepReplacing(z," "," ")z=KeepReplacing(z,"|","."&gb.NewLine)Ifta1.Selection.Length=0Thenta1.Text=zElseta1.Selection.Text=zSaveOrUpdateEndPublicSubMenuUndo_Click()DimzAsString=ta1.Textta1.Text=OriginalTextOriginalText=zSaveOrUpdateEndPublicSubMenuRenumber_Click()mdb.RenumberShowRecordEndPublicSubMenuVacuum_Click()Me.Caption="File size -> "&mdb.Vacuum()EndPublicSubMenuBackup_Click()Me.Caption=mdb.Backup()EndPublicSubMenuTypeExtra_Click()ta1.SetFocusta1.Text&=gb.NewLine&gb.NewLineta1.Select(ta1.Text.Len)EndPublicSubMenuDoubleSpace_Click()OriginalText=ta1.TextIfIsNull(ta1.Text)ThenReturnDimzAsString=If(ta1.Selection.Length=0,Trim(ta1.Text),Trim(ta1.Selection.Text))z=KeepReplacing(z,gb.NewLine,"|")z=KeepReplacing(z,"||","|")z=KeepReplacing(z,"|",gb.NewLine&gb.NewLine)Ifta1.Selection.Length=0Thenta1.Text=zElseta1.Selection.Text=zSaveOrUpdateEndPublicSubti1_MiddleClick()Me.ShowMe.ActivatetbSearch.Text=""mdb.SelectAllNotesShowRecordEndPublicSubMenuHide_Click()Me.HideEnd
The Massage(string) function is necessary for handling the saving of text that has single apostrophes in it. SQL statements use single apostrophes to surround strings. A single apostrophe in the string will terminate the string and what follows will be a syntax error as it will be incomprehensible. To include an apostrophe it has to be doubled. For example, to save the string Fred’s house it has to first be converted (“massaged”) to Fred’’s house.
The KeepReplacing(InThis, LookFor, ReplaceWithThis) function performs replacements until the LookFor string is no longer present. For example, if you wanted to remove multiple x’s from abcxxxxdef and just have one single x you cannot just use Replace(“abcxxxxdef”, “xx”, “x”), for this would produce abcxxdef. The first double-x becomes a single x, and the second double-x becomes a single x. You still have a double-x. You have to keep replacing until there are no more double-x’s.
That’s all, folks, except for the reference appendices. May I finish where I began, with a word of thanks to Benoît Minisini. This programming environment is a delight to use. With the gratitude of all of us users we sing, glass in hand, “For he’s a jolly good fellow, and so say all of us”.
This book is the work of someone who has only picked up Gambas in the last six months. It began as a Christmas holidays project. The only object-oriented programming experience I have had prior to this is with Xojo, another delightful language in which to program, though, unlike Gambas, commercial. Neither has this book been proof read, so there will be errors for sure. Programming is about errors every step of the way. The sample programs have been tested and they work, but even there something may have crept through that needs fixing.
My interest in computers began in 1974 while a student at a teachers college in Brisbane. At that time I was one of a small group whom a lecturer invited to learn programming after hours from an acoustic coupler. The telephone handset was fastened with rubber clips to a teletype and messages went to and from the University of Queensland computer, a massive thing with a whopping 20 megabytes of memory that could handle 64 remote users concurrently. After graduation I went on staff in a primary school in north Queensland, and would often bike over to the secondary school to check out the latest version of a computer language called MBASIC written by some young bloke called Bill Gates.
That was in 1976 and the school was the first in the state to have a computer, a DEC-10. There were no floppy disks in those days: it was all stored on paper tape. It was fascinating to type a command (even the word made you feel powerful) on a keyboard and the paper tape writer somewhere else in the room would punch out confetti from a zigzag strip of paper. It could be read by lights shining through the small holes as they were pulled over the light sensors at the dazzling speed of 300 characters per second.
In one school a staff member couldn’t see what was difficult about using computers to print student reports. “Can’t you just press the PRINT button?” Forty years on I am still writing PRINT buttons. We all pursue the goal of finding the holy grail of programming: one button to rule them all, one button to do everything. Programming has only ever been a hobby, though. That there are people who write languages, database engines and operating systems is awe-inspiring. There are wizards out there. They walk among us. They look like ordinary people.
With that background, can you forgive me for using i and j as names for integer variables? For those who want to, there is http://Gambaswiki.org/wiki/doc/naming where my agricultural standards can be refined. For example,
This book also lacks an introduction to many tools.
Look at them all. All this book provides are buttons, textboxes, labels, grid- and tableviews and forms. The goodies remain in their boxes, unopened, under the Christmas tree. I may find out about them myself someday. They certainly look exciting.
To the person learning Gambas and programming for the first time, good luck.
Thanks again, Benoît and all the writers on forums.
Double-click a blank area of the form to start typing code for the Public Sub Form_Open() event. Double-click a button to start typing code for Public Sub Button_Click(). Otherwise, right-click the object or form > click EVENT… > choose the event you want to write code for.
If you hold down the CTRL key and click on a Gambas reserved word (e.g. Public, New, For, etc) the relevant help page is displayed. Selecting a keyword and pressing F2 also does it.
Right-click a tool in the toolbox and help will appear.
In preferences (Ctrl-Alt-P) switch on Local variable declaration then in your empty 'Sub' type iCount = 6 and a Dim iCount as Integer automatically appears.
Breakpoint set on the Dim statement. SoccerPlayer has “Maradona” in it.
If the program is paused (you put in a “breakpoint” in a certain place in the code, and the execution reached this place, or you click the Pause button) you can select a variable (drag over it, or double-click it) and you will see its value.
If you select the width or height properties of a control, the up or down arrows increase or reduce it by 7 pixels.
If you select the X property of a control, up-arrow moves right, down-arrow moves left by 7 pixels. Similarly in the Y property value box, up-arrow moves down and down-arrow moves up (work that one out) by 7 pixels.
If you want a line or selected lines of code to move up, click in the line and press Alt-UpArrow. Alt-DownArrow moves it or them down.
Select the lines > Press Ctrl-K to “komment-out” the lines. Ctrl-U with lines selected will un-comment them. (Commenting out means making them into comments so they are not executed.)
Every character has an ASCII code, eg Asc("A") is 65.
Chr
Returns a character from its ASCII code
Chr(65) is "A" . Chr(32) is the space. Chr(1) is Ctrl-A
When checking to see if a key was pressed, use Key["A"] or Key[Key.ShiftKey] instead for the number of a key on the keyboard, rather than the number of a character.
Parts of Strings
Left
The left-hand end of a string
Left("Hello", 4) is "Hell"
Mid
The “middle” part of a string
Mid(String, StartingAt, HowLong)
Mid("Gambas", 3, 2) is mb because that is the string that starts at position 3 and is 2 characters long.
Right
The right-hand end of a string
Right("String", 4) is "ring"
Find and Replace
InStr
The position of the second string in the first
InStr(String, LookFor)
InStr(String, LookFor, StartingAt)
InStr("Gambas is basic", "bas") is 4, because bas starts at position 4.
InStr("Gambas is basic", "bas", 6) is 11, because InStr only starts looking for bas from position 6.
If the string is not found it returns 0.
InStr("Gambas is basic", "bas", -5) is 11. The -5 means only start looking five from the end until the end, i.e. at position 10, the second space. Note that the last letter, c, is not 1 from the end. It is the end. i is 1 from the end.
Replace
The string with every occurrence of something replaced by something else
A given date and time is stored internally as two integers, the first being the date and the second being the time. The following examples may explain what you can do with dates and times.
Examples:
Now
07/08/2019 19:18:54.929
8 July 2019, almost 8:19 pm
The current date and time
Format(Now, "dddd dd/mm/yyyy hh:nn:ss")
Tuesday 09/07/2019 20:45:13
More on Format() later. Use it to present a date and/or time, or indeed any number, in the way you want. And it is now 8:46pm on July 9, by the way.
Date()
07/07/2019 23:00:00
When today started, less one hour for Daylight Saving Time.
Date(1955, 6, 1)
05/31/1955 23:00:00
Someone’s birthday, less one hour for Daylight Saving Time. The date is assembled from Year, Month, Day.
Date(2019, 12, 25, 6, 15, 0)
12/25/2019 05:15:00
6:15 am on Christmas Day, 2019. Time for opening Christmas presents.
Year, Month, Day, Hours, Minutes, Seconds
DateAdd(Date(1955, 6, 1), gb.Day, 14)
06/14/1955 23:00:00
14 days after 1 June 1955. DS again.
You can add other things besides days:
gb.Second, gb.Minute, gb.Hour,
gb.Day, gb.Week, gb.WeekDay (ignore Saturday and Sunday),
2427551928 … no, wait, it’s 2427552144 … no, wait, it’s 2427552216 ...
How many heartbeats since my birth.
Had to convert the DateDiff to Long Integer because without it there is an overflow problem: it shows as a negative number. Longs can hold very big numbers.
Parts of Dates and Times
Hour(Now) & " hrs " & Minute(Now) & " mins"
14 hrs 39 mins
It is now 2:39 pm.
You can also use these:
Day(), Hour(), Minute(), Month()
Second(), Time(), Week(), Weekday()
Year()
"Rip van Winkle, this is " & Year(Now)
Rip van Winkle, this is 2019
The year part of a date
WeekDay(Now)
2
0 = Sunday, 6 = Saturday
2 means it is Tuesday.
If WeekDay(Now) = gb.Tuesday then label1.text = "It is Tuesday." else label1.text = "It is not Tuesday."
It is Tuesday.
gb.Tuesday is a constant whose value is 2. Using the constant means you need not remember Tuesday is 2.
Others are gb.Monday, gb.Wednesday, gb.Thursday, gb.Friday, gb.Saturday and gb.Sunday
Time(Now)
14:58:44.654
The time part of a date (Now is the current date and time)
Time(14, 08, 25)
14:08:25
Assemble a time from its parts
Time(Hour, Minutes, Seconds)
Time()
15:05:09.515
The time right now
Conversions
Val("1/6/55")
05/31/1955 23:00:00
Converts the string to a date
The 1-hour difference is due to daylight saving time.
Val("1/6/55 2:00")
06/01/1955 01:00:00
2 am on 1 June 1955
The 1-hour difference is due to daylight saving time.
Val("1-6-55 2:00")
nothing
Very sensitive to the format… ignored
Str(2484515)
2484515
If you supply a number, Str() will convert that number to a string.
Concatenate two strings that contain file names. Add a path separator between the two strings if necessary.
An example of a path is
/home/gerard/Documents/Gambas/
How to get it:
User.Home &/ "Documents" &/ "Gambas/"
Comparison
String = String
Returns if two strings are equal.
String == String
Case-insensitive comparison
Returns if two strings are equal, regardless of upper or lower case
String LIKE String
Checks if a string matches a pattern. Is the first string like the second? There are special codes for the pattern string. Refer to the wiki for more codes, http://Gambaswiki.org/wiki/lang/like
* means any character or string of characters
"Gambas" Like "G*" means “Does Gambas begin with a G?”
? means any single character; [ ] means either/or the things in brackets:
"Gambas" Like "?[Aa]*" means “Does Gambas have a capital or small A as its second letter?”
"Gambas" Like "G[^Aa]*" means “Does Gambas not have a capital or small A as its second letter?”
One byte is the amount of memory required to store a single character such as the letter “A” or the digit “1”. It is eight bits (1’s or 0’s, like little switches that are on or off, up or down). 8 bits = 1 byte. 4 bits = 1 nibble, but that one is not used much.
Some conversions are omitted. Many of these conversions are done automatically as required. For example, these are fine without explicitly having to write the functions:
Write a date only if the date and time value has a date part, and write a time only if it has a date part. Writes nothing for a null date or a short time when there is no date, and writes the date and time for all other cases.
Format(Now, gb.GeneralDate) is 10/07/2019 21:17:45
gb.Standard
Uses gb.GeneralNumber for formatting numbers and gb.GeneralDate for formatting dates and times.
In an expression, which part gets worked out first, then which operations are worked out next?
For example, is 2 + 3 * 4 equal to 20 (+ first, * second) or is it 14 (* first, then +)? Multiplication precedes addition, so the expression comes down to 14.
Rules:
Things are worked out before they are compared.
Anything in brackets is worked out first.
The highest priority is changing the sign (–) or reversing true/false with NOT.
Strings are joined before paths are assembled. (& is done before &/).
Powers are done before multiplication or division, which are done before addition or subtraction.
Where there are several operations and they all have the same precedence, the order is left to right but it does not matter because 3 * (4 / 2) is the same as (3 * 4) / 2
Comparisons are worked out before they are ANDed, ORed or XORed with other comparisons.
Examples:
4 ^ 2 * 3 ^ 3 is the same as (4 ^ 2) * ( 3 ^ 3 )
a > 10 AND a < 20 is the same as ( a > 10 ) AND ( a < 20 )
4 * 2 + 3 * 3 is the same as ( 4 * 2 ) + ( 3 * 3 )
4 + 2 = 5 + 1 is the same as ( 4 + 2 ) = ( 5 + 1 )