Happy #MapInfoMonday!
It's been a few weeks so it's time to continue our work on the MapBasic introduction. This session builds on the previous session: Introducing MapBasic Projects.
Until now, we have added a couple of tables to the tool that allows the user to open and close these with a single click of a button. This works quite fine but we can also see that the tool isn't very flexible. If a user wants another table added, we need to hardcode this into the tool, capture the layer settings, and hardcode these into the application.
So, today, we will remove the current way of handling tables, and build a new way. The user will drive this and the tool therefore needs to be able o read and write settings to and from a configuration file.
We will start with a simple way of capturing new tables, storing the layer settings, and adding buttons for this new table. Let me sketch out the process:
1. The user clicks a button on the ribbon
2. The tool looks for a map window and uses the first layer in this map window
3. The user is asked to select an image file for the icon on the button
4. The tool captures the layer style and label settings
5. The name of the new button is written in a configuration file along with the icon file and the layer settings.
6. A new button is added to the ribbon using the name and the settings from the configuration file
We will use more of the modules from the Common MapBasic Library. I hope this will show you that you can do a lot with just a few lines of code when you take advantage of existing functionality.
Below, you can see the final tool where I have caused it to add four buttons to easily open tables into a map.
Main Procedure
In the Main
procedure, we change the way we build the ribbon. Instead of hardcoding a couple of buttons, we will add a new button that allows the user to add his own buttons to the ribbon. I will add these to a separate group Easy Open Table on the Home tab.
After this, we will read the configuration file for tables to add buttons for.
At the top of the source file, I start by defining my configuration file like this:
Define FILE_INI ApplicationDirectory$() & "EasyOpenTables.ini"
In my code, I can refer to the configuration file using FILE_INI
. This file will be stored in the same folder as the application. I could also have stored it in AppData folder of the user if I preferred.
I also create a variable at a modular level to hold the names of the buttons that I add. This variable is an array of type string
. An array is basically a list of multiple variables where I can pinpoint each using an index. It's defined like this:
Dim marrEasyOpenNames() As String
I haven't given it any size as I don't know that yet. This will be determined as I read the configuration file.
As I mentioned above, I will change how I build the ribbon. Here's the new Main
procedure where the ribbon gets built.
Sub Main
Dim nEasyOpen As Integer,
sName As String
Create ButtonPad "Easy Open Tables" As
PushButton
Calling AddTableToEasyOpen
Large Icon -1 File MI_IMG_MAP_NEWMAPPER_32
HelpMsg "Add Table to EasyOpen\nAdd Table"
Separator
Tab TAB_HOME
nEasyOpen = 0
Do While TRUE
nEasyOpen = nEasyOpen + 1
sName = CONFIGReadKey(FILE_INI, "EASYOPENTABLE", "NAME" + nEasyOpen)
If sName = "" Then
Exit Do
End If
Call ARRAYAddValueString(marrEasyOpenNames, sName)
Call AddButtonEasyOpen(sName, nEasyOpen)
Loop
End Sub
Let me give you a quick overview of this.
The initial Create Button
statement adds a single button to the Home tab. This button is created in its own group called Easy Open Tables.
I then use a Do While ... Loop
statement to add any user-defined buttons to the ribbon. This loop will continue looping until I break out of it inside the loop. I do this, when I don't get a name from the configuration file for a given button number:
sName = CONFIGReadKey(FILE_INI, "EASYOPENTABLE", "NAME" + nEasyOpen)
I use a custom-built function to read values from my configuration file. The structure of the configuration file looks like this for 2 buttons:
[EASYOPENTABLES]
NAME1=Point of Presence
TABFILE1=C:\3. Demo\2. Maps\2023\Site Selection\Fiber Network\PointOfPrecense.TAB
ICONFILE1=C:\Horsboll-Moller Dropbox\Peter Horsbøll Møller\3. MB_Kode\Intro to MapBasic\PointofPrecense 64x64.png
NAME2=Cable
TABFILE2=C:\3. Demo\2. Maps\2023\Site Selection\Fiber Network\Cable.TAB
ICONFILE2=C:\Horsboll-Moller Dropbox\Peter Horsbøll Møller\3. MB_Kode\Intro to MapBasic\Cable 64x64.png
[EASYOPENLAYERS]
LAYER1=F;T;F;0;100000;m;1;F;F;F;255;2,2,16728064;2,2,3175935;1,16777215,16777215;Precision Sans W,0,10,0,-1;2,35,16744448,4,MapInfo Symbols,256,0;F;0
LABEL1=F;3;0;100000;m;ID;2;Precision Sans W,0,10,0,-1;5;F;T;2;2147483647;F;1;255;F;T;T;50;F;F;F;ID;F;2;F;1,2,0;40
LAYER2=F;T;F;0;100000;m;1;F;F;F;255;2,2,16728064;2,2,3175935;1,16777215,16777215;Precision Sans W,0,10,0,-1;2,35,16744448,4,MapInfo Symbols,256,0;F;0
LABEL2=F;3;0;100000;m;ID;2;Precision Sans W,0,10,0,-1;2;F;F;2;2147483647;F;1;255;F;T;T;50;F;F;F;ID;F;1;F;1,2,0;40
It has two sections, EASYOPENTABLES
and EASYOPENLAYERS
. The first contains keys for the name of the layer, the name of the tab file, and the name of the icon file. The second holds keys for the details of the layer settings and the label settings.
Notice how the function CONFIGReadKey
takes three parameters: the name of the configuration file, the name of the section, and the name of the key. This makes it easy to read specific keys from the configuration file.
If this function call doesn't return a name, no button exists using that ID (nEasyOpen
), and the code goes out of the Do
loop.
If a name is returned, I call two procedures. First, I call a procedure to add the name to my array of button names. Then, I call a procedure to add the button to the ribbon. The first procedure I call (ARRAYAddValueString
) can be found in the ARRAYLib module. Let's have a look at the second procedure (AddButtonEasyOpen
).
The first time you run the tool, it will look like this with just the Add Table button added to the Home tab
AddButtonEasyOpen Procedure
The procedure AddButtonEasyOpen
takes two parameters: the name of the button and the ID or number of the button.
Sub AddButtonEasyOpen( ByVal sName As String
, ByVal nNumEasyOpen As Integer
)
Dim sIconFile As String
sIconFile = CONFIGReadKey(FILE_INI, "EASYOPENTABLES", "ICONFILE" + nNumEasyOpen)
Alter ButtonPad "Easy Open Tables"
Add
PushButton
Calling OpenEasyOpenTable
Large Icon -1 File sIconFile
HelpMsg "Open Table '" & sName & "'\n" & sName
ID 11000 + nNumEasyOpen
Tab TAB_HOME
End Sub
As I may have to add many buttons, I decided to create this as its own procedure which I can call from several places in my code. Until now, I have only shown that I call it from the Main
procedure. I will also call it from the procedure where the user adds a new button (AddTableToEasyOpen
). I will get to this procedure later.
In the procedure, I use CONFIGReadKey to get to the name of the icon file for the button, and then I add a button to the Easy Open Tables group. The button calls a generic procedure that will handle the opening of the table when the user clicks on the button. The help message for the button is also generic but takes the name of the button into account as well. Finally, I give the button an ID: 11000 + nNumEasyOpen
. I need this ID to determine which button was clicked by the user in the procedure OpenEasyOpenTable
. I need this so that I know what table the user wants to open.
AddTableToEasyOpen Procedure
Let's have a look at the procedure that gets called when the user wants to create a new button to open a table.
Sub AddTableToEasyOpen
Dim nMID, nLayer, nNumEasyOpen As Integer,
sTab, sName, sIconFile As String,
tlay As T_LAYER_SETTINGS,
tlbl As T_LABEL_SETTINGS
nMID = FrontWindow()
If nMID = 0 Then
Note "Please have a map window as your active window!"
Exit Sub
End If
If Not WindowInfo(nMID, WIN_INFO_TYPE) = WIN_MAPPER Then
Note "Please have a map window as your active window!"
Exit Sub
End If
nLayer = 1
sTab = LayerInfo(nMID, nLayer, LAYER_INFO_NAME)
sName = LayerInfo(nMID, nLayer, LAYER_INFO_FRIENDLYNAME)
If sName = "" Then
sName = sTab
End If
sIconFile = FileOpenDlg(ApplicationDirectory$(), "", "PNG", "Select icon file for table '" & sName & "'...")
If sIconFile = "" Then
sIconFile = MI_IMG_APP_OPENTABLE_32
End If
Call ARRAYAddValueString(marrEasyOpenNames, sName)
nNumEasyOpen = Ubound(marrEasyOpenNames)
Call CONFIGWriteKey(FILE_INI, "EASYOPENTABLES", "NAME" + nNumEasyOpen, sName)
Call CONFIGWriteKey(FILE_INI, "EASYOPENTABLES", "TABFILE" + nNumEasyOpen, TableInfo(sTab, TAB_INFO_TABFILE))
Call CONFIGWriteKey(FILE_INI, "EASYOPENTABLES", "ICONFILE" + nNumEasyOpen, sIconFile)
Call tlsGetInfo(nMID, nLayer, tlay)
Call tlbsGetInfo(nMID, nLayer, tlbl)
Call CONFIGWriteKey(FILE_INI, "EASYOPENLAYERS", "LAYER" + nNumEasyOpen, tlsToString(tlay))
Call CONFIGWriteKey(FILE_INI, "EASYOPENLAYERS", "LABEL" + nNumEasyOpen, tlbsToString(tlbl))
Call AddButtonEasyOpen(sName, nNumEasyOpen)
End Sub
At the top, I define the variables that I need. There are two special variables that I'd better introduce specifically: tlay As T_LAYER_SETTINGS
and tlbl As T_LABEL_SETTINGS
. They aren't normal variables but Types. A type is a special struct that allows you to create a variable with multiple values of various types. An array also allows you to define a variable with multiple values but they are all of the same type.
Here is the type T_LAYER_SETTINGS
:
Type T_LAYER_SETTINGS
bEditable As Logical
bSelectable As Logical
bZoomLayered As Logical
fMinZoom As Float
fMaxZoom As Float
sZoomUnits As String
nDisplay As Integer
bShowLineDirection As Logical
bShowNodes As Logical
bShowCentroids As Logical
nAlpha As Integer
penOverrideLine() As Pen
penOverrideBorder() As Pen
brsOverride() As Brush
symOverride() As Symbol
fntOverride() As Font
bTrancparency As Logical
nTransparencyColour As Integer
End Type
As you can see it holds 18 different values of various types. Some of these are even arrays, such as penOverrideLine() As Pen
. In this case, these two types are custom-built and part of a separate module that helps you work with these types. The modules allow you to capture layer and label settings from a layer and set these settings back onto a layer again at a later stage. The modules also have functions that allow you to convert the type into a single string that can be stored, for instance in a configuration file.
I'll show you how to use these modules when we get to it.
After the variables, the procedure checks that the active window is a map window, and then it gets some information from the first layer of this map. At a later stage, we can change this and let the user select the layer to use. For now, we keep it simple and use the first layer.
I use the function FileOpenDlg
to ask the user to select an image file from the disk. This file will be used on the button. If the user cancels the dialog, I use a default Open Table icon.
I add the friendly name of the layer to my list of Easy Open Names, and write the details to the configuration file using the CONFIGWriteKey
procedure. This procedure works similarly to the CONFIGReadKey function but writes instead of reads.
I now use the before-mentioned modules to read settings from the layer:
Call tlsGetInfo(nMID, nLayer, tlay)
Call tlbsGetInfo(nMID, nLayer, tlbl)
The first procedure reads the settings from the layer itself, and the second reads the label settings from the layer. These settings get stored in the two variables tlay
and tlbl
.
Now it's time to write the settings to the configuration file. I use CONFIGWriteKey to write the setting to the file, and I use two functions to convert the Type variables to a single string: tlsToString
and tlbsToString
.
Call CONFIGWriteKey(FILE_INI, "EASYOPENLAYERS", "LAYER" + nNumEasyOpen, tlsToString(tlay))
Call CONFIGWriteKey(FILE_INI, "EASYOPENLAYERS", "LABEL" + nNumEasyOpen, tlbsToString(tlbl))
Finally, I call the procedure AddButtonEasyOpen
to add the new button to the ribbon
OpenEasyOpenTable Procedure
I also needed to modify the way I open the tables. I will use the existing function OpenTableIntoMap
and procedure ConfigureLayer
to do the actual work. The procedure does need some modification too.
Sub OpenEasyOpenTable
Dim nMID, nNumEasyOpen As Integer,
sTab, sTabFile As String
nNumEasyOpen = CommandInfo(CMD_INFO_MENUITEM) - 11000
sTabFile = CONFIGReadKey(FILE_INI, "EASYOPENTABLES", "TABFILE" + nNumEasyOpen)
sTab = PathToTableName$(sTabFile)
If TABIsOpen(sTab) Then
Close Table sTab
Else
nMID = OpenTableIntoMap(sTabFile)
Call ConfigureLayer(nMID, sTabFile, marrEasyOpenNames(nNumEasyOpen ), nNumEasyOpen)
End If
End Sub
After the variables, I use the MapBasic function CommandInfo
to determine which button the user clicked on. This helps me determine the table to open.
I then get the name of the table from the configuration file using the function CONFIGReadKey
. If the table is open, I close it. If it isn't open, I open it and add it to the map using the modified procedure ConfigureLayer
.
ConfigureLayer procedure
I added two more parameters to the procedure ConfigureLayer. The name of the button and the ID or number of the button too.
I also added a new variable tli
of type T_LAYER_INFO
to the procedure. This variable is also a type and will help me add the settings to the layer. Just like the two types I mentioned earlier, this type also comes with a custom-built module that makes it easier to work with the type.
Sub ConfigureLayer( ByVal nMID As Integer
, ByVal sTabFile As String
, ByVal sName As String
, ByVal nNumEasyOpen As Integer
)
Dim sTab, sValue As String,
nLayerID As Integer,
bSomeSet As Logical,
tli As T_LAYER_INFO
sTab = PathToTableName$(sTabFile)
nLayerID = LAYERGetLayerNumber(nMID, sTab)
bSomeSet = FALSE
If nLayerID > 0 Then
tli.nID = nLayerID
tli.nMID = nMID
tli.sName = sName
tli.nType = LayerInfo(nMID, nLayerID, LAYER_INFO_TYPE)
If sTab <> sName Then
Set Map Window nMID Layer nLayerID FriendlyName sName
End If
'**Getting the layer settings
sValue = CONFIGReadKey(FILE_INI, "EASYOPENLAYERS", "LAYER" + nNumEasyOpen)
If sValue <> "" Then
If tlsFromString(tli.tlsLyrSettings, sValue) Then
bSomeSet = TRUE
End If
End If
'**Getting the label settings
sValue = CONFIGReadKey(FILE_INI, "EASYOPENLAYERS", "LABEL" + nNumEasyOpen)
If sValue <> "" Then
If tlbsFromString(tli.tlbsLblSettings, sValue) Then
bSomeSet = TRUE
End If
End If
'**Applying the layer and/or label settings
If bSomeSet Then
If tliApplyInfo(nMID, nLayerID, tli) Then
End If
End If
End If
End Sub
I read the layer settings from the configuration file and converts these back to the types using tlsFromString
and tlbsFromString
. I store the converted settings in the two variables of the tli variable: tli.tlsLyrSettings
and tli.tlbsLblSettings
.
If I was able to get some settings from the configuration file, I apply them to the layer using this line:
If tliApplyInfo(nMID, nLayerID, tli) Then
End If
The function tliApplyInfo takes three parameters: the window ID, the layer ID and the T_LAYER_INFO
type holding the layer settings.
Additional Modules
I end up using a number of additional modules from the Common MapBasic library. These are the modules I include at the top of my source file:
Include "Library\ARRAYLib.def"
Include "Library\CONFIGFILELib.def"
Include "Library\LAYERLib.def"
Include "Library\TABLELIb.def"
Include "Library\Types\T_LABEL_SETTINGS.def"
Include "Library\Types\T_LAYER_SETTINGS.def"
Include "Library\Types\T_LAYER_INFO.def"
Notice that the types are in the Types subfolder.
I also need to include additional compiled MapBasic Object files in my MapBasic Project File. The project file looks like this:
[LINK]
Application=EasyOpenTables.mbx
Module=Library\ARRAYLib.mbo
Module=Library\COLUMNLib.mbo
Module=Library\DEBUGLib.mbo
Module=Library\ERRORLib.mbo
Module=Library\LAYERLib.mbo
Module=Library\STRINGLib.mbo
Module=Library\STYLELib.mbo
Module=Library\TABLELib.mbo
Module=Library\CONFIGFILELIb.mbo
Module=Library\Types\T_LABEL_SETTINGS.mbo
Module=Library\Types\T_LAYER_SETTINGS.mbo
Module=Library\Types\T_LAYER_INFO.mbo
Module=06 Reading Layer Settings.mbo
I'm not using functions or procedures from all these modules. But some of the modules that I include in my source file may use a function or procedure from one of these and so I need to include these in my MapBasic project.
I have attached a zip file with the final version of the tool. Feel free to grab the file and play with it.
------------------------------
Peter Horsbøll Møller
Principal Presales Consultant | Distinguished Engineer
Precisely | Trust in Data
------------------------------