MapInfo Pro

 View Only

MapBasic Monday: Adding User Flexibility to the tool

  • 1.  MapBasic Monday: Adding User Flexibility to the tool

    Posted 05-08-2023 05:48
      |   view attached

    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
    			Calling AddTableToEasyOpen
    			Large Icon -1 File MI_IMG_MAP_NEWMAPPER_32	
    			HelpMsg "Add Table to EasyOpen\nAdd Table"
    		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)
    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:

    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
    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
    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"
    			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", "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:

    	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
    		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:

    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