MapInfo Pro Developers User Group

Expand all | Collapse all

Using Python Script to Customize MapInfo Pro UI and Create Dialogs

  • 1.  Using Python Script to Customize MapInfo Pro UI and Create Dialogs

    Employee
    Posted 8 days ago
    Edited by Anshul Goel 8 days ago
    Based on the Continued content about using Python script to create addins for MapInfo Pro, This post will help you in creating a Python Addin which will allow to customize the MapInfo Pro Ribbon, add some button and Create Dialogs.

    Before we start, Please have a look at the post which will give you more information regarding the extent of support for Python in MapInfo Pro.

    Now let get started:

    Setup

    1. To Create an Addin in Python, we will first start by taking the Simple template provided in the MapBasic installation or attached. This template already has the startup code in main() class for MapInfo Pro Python Addin. 
      • SAMPLES\RIBBONINTERFACE\Python\py_addin_templates\Simple
    2. Once the template copied, we can start modifying it.
      • Start with renaming the files → change <renameHere> to your addin name.
      • Rename module reference as per your new file names.
      • Check for all TODO in python code and modify the code as per your need.

    Modifying the Ribbon

    In your new Python module created above, lets look at the main() class, which is responsible to initialize and load a Python AddIn. main class should always be present if writing an AddIn or the Python module will be treated as standalone script.
    # this class is needed with same name in order to load the python addin and can be copied 
    # as it is when creating another addin.
    class main():
        def __init__(self, imapinfopro):
            # imapinfopro is the object of type MapInfo.Type.IMapInfoPro 
            # giving access to the current running instance of MapInfo Pro.
            self._imapinfopro = imapinfopro
    
        def load(self):
            try:
                # uncomment these lines to debug the python script using VSCODE
                # Install ptvsd package into your environment using "pip install ptvsd"
                # Debug in VSCODE with Python: Attach configuration
    
                # ptvsd.enable_attach()
                # ptvsd.wait_for_attach()
                # ptvsd.break_into_debugger()
    
                # here initialize the addin class
                if self._imapinfopro:
                    # obtain the handle to current mbx application
                    thisApplication = self._imapinfopro.GetMapBasicApplication(os.path.splitext(__file__)[0] + ".mbx")
                    self._addin = MyAddin(self._imapinfopro, thisApplication)
            except Exception as e:
                print("Failed to load: {}".format(e))
        
        def unload(self):
            try:
                if self._addin:
                    self._addin.unload()
                    del self._addin
                self._addin = None
            except Exception as e:
                print("Failed to unload: {}".format(e))
        
        def __del__(self):
            self._imapinfopro = None
            pass
    
        # optional -- tool name that shows in tool manager
        def addin_name(self) -> str:
            # TODO: change here
            return "Python Add-in"
    
        # optional -- description that shows in tool manager
        def addin_description(self) -> str:
            # TODO: change here
            return "Python Add-in Description"
        
        # optional -- default command text in  tool manager
        def addin_defaultcommandtext(self) -> str:
            # TODO: change here
            return "Python Add-in Default Command"
    
        # optional -- default command when run or double-clicked in tool manager
        def addin_defaultcommand(self):
            # TODO: change here
            self.on_default_button_clicked(self)
    
        # optional -- image that  shows in tool manager
        def addin_imageuri(self) -> str:
            # TODO: change here
            return "pack://application:,,,/MapInfo.StyleResources;component/Images/Application/about_32x32.png"
    
        def on_default_button_clicked(self, sender):
            # TODO: change here
            try:
                print('default command executed')
            except Exception as e:
                print("Failed to execute: {}".format(e))

    In the above main class, we are creating MyAddin class which is where we will start adding our code.
    # TODO: change your addin class name here.
    class MyAddin():
        def __init__(self, imapinfopro, thisApplication):
            try:
                self._pro = imapinfopro
                self._thisApplication = thisApplication
    
                # TODO: Add your code here for addin initialization.
            except Exception as e:
                print("Failed to load: {}".format(e))
    
        def unload(self):
            # TODO: Add your code here for unloading addin.
            self._thisApplication = None
            self._pro = None
            pass

    Lets first start with modifying the MapInfo Pro Ribbon. We will create following things in MapInfo Pro Ribbon.
    • New Ribbon Tab
      • New Ribbon Group inside the Tab
        • A Standard Tool Button
        • A Custom Tool Button invoking a Python method.
        • Button Creating Windows Forms Dialog.
        • Button Creating A WPF Dialog.

    All the above modification will be done to the UI when MyAddin class object is created, therefore we to have modified the __init__ and unload method of MyAddin class as below.

    def __init__(self, imapinfopro, thisApplication):
            try:
                self._pro = imapinfopro
                self._newTab = None
                self._thisApplication = thisApplication
                # Create A new Ribbon Tab and Add controls.
                self.AddNewTabToRibbon()
            except Exception as e:
                print("Failed to load: {}".format(e))
    
    def AddNewTabToRibbon(self):
            pass
    
    def unload(self):
            if self._newTab:
                self._pro.Ribbon.Tabs.Remove(self._newTab)
            self._newTab = None
            self._thisApplication = None
            self._pro = None

    Add a new Ribbon Tab and Ribbon Group
    def AddNewTabToRibbon(self):
            # Create a New Ribbon Tab
            self._newTab = self._pro.Ribbon.Tabs.Add("RibbonCustomization","Ribbon Customization")
            if self._newTab:
                # Create a New Ribbon Group
                tabGroup = self._newTab.Groups.Add("DemoControls", "Demo Controls")
    
                # Now we can start adding controls in the Ribbon Group
                if tabGroup:
                    tabGroup.IsLauncherVisible = False

    Once the Ribbon Group is added, let us add Button, ToolButtons to Ribbon and attach custom Python methods on button click.
        def AddNewTabToRibbon(self):
            # Create a New Ribbon Tab
            self._newTab = self._pro.Ribbon.Tabs.Add("RibbonCustomization","Ribbon Customization")
            if self._newTab:
                # Create a New Ribbon Group
                tabGroup = self._newTab.Groups.Add("DemoControls", "Demo Controls")
    
                # Now we can start adding controls in the Ribbon Group
                if tabGroup:
                    tabGroup.IsLauncherVisible = False
    
                    # Add a standard tool button
                    buttonZoomIn = tabGroup.Controls.Add("ZoomInToolButton", "Zoom-In Tool", ControlType.ToolButton)
                    if buttonZoomIn:
                        buttonZoomIn.IsLarge = True
                        buttonZoomIn.KeyTip = "ZI"
                        buttonZoomIn.ToolTip = AddinUtil.create_tooltip("Zoom In", "Zoom In", "Open Mapper to use")
                        buttonZoomIn.LargeIcon = CommonUtil.path_to_uri("pack://application:,,,/MapInfo.StyleResources;component/Images/Mapping/zoomIn_32x32.png")
                        buttonZoomIn.SmallIcon = CommonUtil.path_to_uri("pack://application:,,,/MapInfo.StyleResources;component/Images/Mapping/zoomIn_16x16.png")
                        buttonZoomIn.CommandId = 1705
                        buttonZoomIn.Cursor = "129"
                        buttonZoomIn.DrawMode = 30
                        buttonZoomIn.BModifier = True
                
                    # Add a custom tool button
                    buttonCustomInfoTool = tabGroup.Controls.Add("InfoToolButton", "Info", ControlType.ToolButton)
                    if buttonCustomInfoTool:
                        buttonCustomInfoTool.IsLarge = True
                        buttonCustomInfoTool.KeyTip = "TI"
                        buttonCustomInfoTool.ToolTip = AddinUtil.create_tooltip("Info Tool", "Info Tool", "Open Map window to enable the tool.")
                        buttonCustomInfoTool.LargeIcon = CommonUtil.path_to_uri("pack://application:,,,/MapInfo.StyleResources;component/Images/Mapping/infoTool_32x32.png")
                        buttonCustomInfoTool.SmallIcon = CommonUtil.path_to_uri("pack://application:,,,/MapInfo.StyleResources;component/Images/Mapping/infoTool_16x16.png")
                        buttonCustomInfoTool.Cursor = "164"
                        buttonCustomInfoTool.DrawMode = 34
                        buttonCustomInfoTool.BModifierKeys = False
                        # attach a command invoking a python function
                        buttonCustomInfoTool.Command = AddinUtil.create_command(self.on_custom_info_tool_clicked)
    
                    tabGroup.Controls.Add("demoSeparator","Demo Seperator",ControlType.RibbonSeparator)
    
                    buttonOpenDialogWinForms = tabGroup.Controls.Add("OpenCustomDialogWinForms", "Open Dialog WinForms", ControlType.Button)
                    if buttonOpenDialogWinForms:
                        buttonOpenDialogWinForms.ToolTip = AddinUtil.create_tooltip("Open Dialog","Open Dialog", "Open Dialog")
                        # attach a command invoking a python function
                        buttonOpenDialogWinForms.Command = AddinUtil.create_command(self.on_open_dialog_winforms_click)
                        buttonOpenDialogWinForms.LargeIcon = CommonUtil.path_to_uri("pack://application:,,,/MapInfo.StyleResources;component/Images/Application/exportAsImage_32x32.png")
                        buttonOpenDialogWinForms.SmallIcon = CommonUtil.path_to_uri("pack://application:,,,/MapInfo.StyleResources;component/Images/Application/exportAsImage_16x16.png")
    
                    buttonOpenDialogWpf = tabGroup.Controls.Add("OpenCustomDialogWPF", "Open Dialog WPF", ControlType.Button)
                    if buttonOpenDialogWpf:
                        buttonOpenDialogWpf.ToolTip = AddinUtil.create_tooltip("Open Dialog","Open Dialog", "Open Dialog")
                        # attach a command invoking a python function
                        buttonOpenDialogWpf.Command = AddinUtil.create_command(self.on_open_dialog_wpf_click)
                        buttonOpenDialogWpf.LargeIcon = CommonUtil.path_to_uri("pack://application:,,,/MapInfo.StyleResources;component/Images/Application/exportAsImage_32x32.png")
                        buttonOpenDialogWpf.SmallIcon = CommonUtil.path_to_uri("pack://application:,,,/MapInfo.StyleResources;component/Images/Application/exportAsImage_16x16.png")
    
                    endProgram = tabGroup.Controls.Add("EndProgram", "End Program", ControlType.Button)
                    if endProgram:
                        endProgram.KeyTip = "LE"
                        endProgram.ToolTip = AddinUtil.create_tooltip("End Program Tooltip Description","End Program ToolTip Text", "End Program Disabled Text")
                        # attach a command invoking a python function
                        endProgram.Command = AddinUtil.create_command(self.on_end_application)
                        endProgram.LargeIcon = CommonUtil.path_to_uri("pack://application:,,,/MapInfo.StyleResources;component/Images/Table/closeTable_32x32.png")
                        endProgram.SmallIcon = CommonUtil.path_to_uri("pack://application:,,,/MapInfo.StyleResources;component/Images/Table/closeTable_16x16.png")
    
        def on_open_dialog_winforms_click(self, sender):
            try:
                pass
            except Exception as e:
                print("Failed to execute: {}".format(e))
    
        def on_open_dialog_wpf_click(self, sender):
            try:
                pass
            except Exception as e:
                print("Error: {}".format(e))
    
        def on_custom_info_tool_clicked(self, sender):
            try:
                x1 = CommonUtil.eval("commandinfo(1)")
                y1 = CommonUtil.eval("commandinfo(2)")
                # show a notification bubble on status bar.
                notification = NotificationObject()
                if notification:
                    formatted_msg = "X: {}\nY: {}".format(x1, y1)
                    CommonUtil.do("print \"{}\"".format(formatted_msg))
                    notification.Message = formatted_msg
                    notification.Title = "Map Coordinates"
                    notification.Type = NotificationType.Info
                    notification.TimeToShow = 2000
                    # MapInfo.Types.FrameworkElementExtension.GetScreenCoordinate method can also be used
                    # to get the screen coordinates of any FrameworkElement.
                    notification.NotificationLocation = Point(-1,-1)
    
                    self._pro.ShowNotification(notification)
            except Exception as e:
                print("Failed to execute: {}".format(e))
    
        def on_end_application(self, sender):
            try:
                if self._thisApplication:
                    self._thisApplication.EndApplication()
            except Exception as e:
                CommonUtil.sprint("Failed to load: {}".format(e))
    


    Creating a WinForms Dialog

    To create a WinForms dialog we will add a new class HelloForm in a separate Python module custom_winforms.py.

    • Class HelloForm extends WinForm.Form C# class
    • Adds Button to the Form. 
    • Attach a python method to click event of Button and execute Note mapbasic statement inside the event handler.
    import clr
    
    clr.AddReference("System.Windows.Forms")
    import System.Windows.Forms as WinForms
    
    from System.Drawing import Size, Point
    from mi_common_util import *
    
    class HelloForm(WinForms.Form):
        def __init__(self, parent):
            self.AutoScaleBaseSize = Size(8, 16)
            self.AutoScaleMode = WinForms.AutoScaleMode.Font
            self.ClientSize = Size(352, 212)
            self.Name = "Form1"
            self.Text = "Custom Dialog"
            self.StartPosition = WinForms.FormStartPosition.CenterParent
            self.Parent = WinForms.Control.FromHandle(parent)
    
            # Create the button
            self.button = WinForms.Button()
            self.button.Location = Point(13, 85)
            self.button.Size = Size(327, 23)
            self.button.Text = "Click Me!"
    
            # Register the event handler
            self.button.Click += self.button_Click
    
            # Add the controls to the form
            self.Controls.Add(self.button)
    
        def button_Click(self, sender, args):
            """Button click event handler"""
            CommonUtil.do("note \"Button Click\"")


    To Show the WinForms Dialog on click of Ribbon Button, let us modify the  on_open_dialog_winforms_click on MyAddin class.

    def on_open_dialog_winforms_click(self, sender):
            try:
                form = HelloForm(self._pro.MainHwnd)
                if form:
                    form.ShowDialog()
            except Exception as e:
                print("Failed to execute: {}".format(e))



    Creating a WPF Dialog

    First create a XAML for the dialog UI. UI that we have created has ContentControl to show Map and as three button at bottom of the dialog.

    Note : When created a XAML UI, we need to make sure to give x:Name to all the controls in the XAML so that Python can easily find the controls.

    <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    				Title="Custom Dialog"
    				Height="400" Width="400"
    				ShowInTaskbar="False"
    				WindowStartupLocation="CenterOwner">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="30"/>
            </Grid.RowDefinitions>
            <ContentControl x:Name="Mapper" Grid.Row="0"/>
            <StackPanel Grid.Row="1" Orientation="Horizontal">
                <Button x:Name="AddMapButton"  Margin="5,5,5,5"  VerticalAlignment="Bottom" IsDefault="False">Add Map</Button>
                <Button x:Name="ShowTableInfo" Margin="5,5,5,5"   VerticalAlignment="Bottom">Table Info</Button>
                <Button x:Name="CloseButton" Margin="5,5,5,5"  VerticalAlignment="Bottom" IsDefault="True" IsCancel="True">Close</Button>
            </StackPanel>
        </Grid>
    </Window>


    In Creating a WPF Dialog we will take MVVM approach, which will have a view, presenter and model.

    We will create a Python module (custom_view_view.py) which will parse this XAML and convert it into a WPF Dialog. This module will be out view.

    from mi_addin_util import AddinUtil
    from os.path import join, dirname
    
    class View:
        XAML_FILE = "custom_view.xaml"
        def __init__(self):
            self._window = None
    
        @property
        def window(self):
            if not self._window:
                self._window = AddinUtil.create_user_control(join(dirname(__file__), self.XAML_FILE))
            return self._window
        
        def _on_close(self, new_value):
            if new_value:
                self.window.Closing += new_value
                self.CloseButton.Click += new_value
        on_close = property(None, _on_close)
        
        def showdialog(self, parent):
            if self._window is not None:
                AddinUtil.set_dialog_parent(self._window, parent)
                return self._window.ShowDialog()
            return None
                
        def _add_map_click(self, new_value):
            if new_value:
                self.AddMapButton.Click += new_value
        add_map_click = property(None, _add_map_click)
    
        def _info_click(self, new_value):
            if new_value:
                self.ShowTableInfo.Click += new_value
        info_click = property(None, _info_click)
    
        def __getattr__(self, name):
            return AddinUtil.find_logical_control(self.window, name)


    The above custom_view_view.py module needs a presenter or controller to handle user interaction/clicks and allow interaction between MapInfo Pro and Python. To all this stuff we will create another Python module custom_view_presenter.py.

    import sys
    from custom_view_model import Model
    from custom_view_view import View
    from MapInfo.Types import MessageOutput
    from mi_addin_util import AddinUtil
    from mi_common_util import CommonUtil
    from System import Int32
    
    # redirect python stdio to Pro
    sys.stdout = sys.stderr = sys.stdin = MessageOutput()
    
    class Presenter:
        def __init__(self, view, imapinfopro):
            self._view = view
            self._pro = imapinfopro
            self._winId = -1
            self._table_name = None
            #self.model = model
    
        def add_map(self, event, sender):
            try:
                if self._winId is not -1:
                    self.close_map()
    
                table_name = input("Table Name")
                if table_name:
                    el, self._winId = self._pro.CreateUnattachedWindow("map from {}".format(table_name), Int32(0))
                    if el:
                        self._view.Mapper.Content = AddinUtil.create_user_control_from_handle(el)
                        self._table_name = table_name
    
            except Exception as e:
                print("Failed to execute: {}".format(e))
    
        def close_map(self):
            try:
                self._view.Mapper.Content = None
                if self._winId is not -1:
                    win = self._pro.Windows.FromId(self._winId)
                    if win:
                        win.Close()
                        self._winId = -1
                        self._table_name = None
            except Exception as e:
                print("Failed to execute: {}".format(e))
        
        def on_close(self, event, sender):
            self.close_map()
    
        def info_click(self, event, sender):
            try:
                if self._table_name:
                    CommonUtil.do("open window message")
                    print(CommonUtil.eval("TableInfo({}, TAB_INFO_NAME)".format(self._table_name)))
                    print(CommonUtil.eval("TableInfo({}, TAB_INFO_NCOLS)".format(self._table_name)))
                    print(CommonUtil.eval("TableInfo({}, TAB_INFO_NROWS)".format(self._table_name)))
                    print(CommonUtil.eval("TableInfo({}, TAB_INFO_MAPPABLE)".format(self._table_name)))
                    print(CommonUtil.eval("TableInfo({}, TAB_INFO_COORDSYS_CLAUSE)".format(self._table_name)))
                    print(CommonUtil.eval("TableInfo({}, TAB_INFO_CHARSET)".format(self._table_name)))
            except Exception as e:
                print("Failed to execute: {}".format(e))
    
        def showdialog(self):
            try:
                self._view.on_close = self.on_close
                self._view.add_map_click = self.add_map
                self._view.info_click = self.info_click
                self._view.showdialog(self._pro.MainHwnd)
            except Exception as e:
                print("Failed to execute: {}".format(e))
    
        @staticmethod
        def create(imapinfopro):
            try:
                return Presenter(View(), imapinfopro)
            except Exception as e:
                print("Error: {}".format(e))
    


    Final step in creating a WPF dialog is to invoke it when user click button on MapInfo Pro Ribbon for which we will again look into MyAddin class and modify  on_open_dialog_wpf_click method.

        def on_open_dialog_wpf_click(self, sender):
            try:
                presenter = Presenter.create(self._pro)
                if presenter:
                    presenter.showdialog()
                presenter = None
            except Exception as e:
                print("Error: {}".format(e))


    We can run this Addin in MapInfo Pro by selecting the module with main() class (py_mapinfo_customization.py) from Run Program dialog.

    This completes our Python Addin development. Final developed addin mentioned in this post is attached.

    Thanks
    Anshul



    ------------------------------
    Anshul Goel
    Knowledge Community Shared Account
    Shelton CT
    ------------------------------

    Attachment(s)

    zip
    Simple.zip   1K 1 version


  • 2.  RE: Using Python Script to Customize MapInfo Pro UI and Create Dialogs

    Employee
    Posted 8 days ago
      |   view attached
    I have updated the attached final addin with latest one.
    Stay Tuned for the next post in thread regarding debugging of Python Addin in Visual Studio Code.

    ------------------------------
    Anshul Goel
    Knowledge Community Shared Account
    Shelton CT
    ------------------------------

    Attachment(s)



  • 3.  RE: Using Python Script to Customize MapInfo Pro UI and Create Dialogs

    Posted 6 days ago
    Thanks Anshul!
    Your post was very helpful. I have another question.

    Can we use Python PYQT5 module created dialogs as well within MapInfo instead of Winform? It is very easy to create dialog in PYQT5designer but I don't know how to make those interacting with MapInfo.
    If this is possible, can you please give some example on how to interact those with MapInfo.

    Thanks,
    Navjot


    ------------------------------
    Navjot Kaur
    Knowledge Community Shared Account
    ------------------------------