Adding your own command buttons to the pcbnew GUI

UPDATE April 14, 2017: You no longer need to use the hardcoded numbers like 6038 mentioned below. You can use pcbnew.ID_H_TOOLBAR.

 

Kicad has three rows of command buttons with predefined functionality. This post is about how to add your own button.

The sample code can be found here.

Perliminaries

Before getting into the code, there are a couple things that are weird about kicad/wxPython’s behavior1

Getting proper aui pointers

The first thing is that if you simply open the python scripting window in pcbnew, find the top level window, and look for the command bars, you’ll likely get a pointer of type wxControl. That type doesn’t have the APIs needed to look at, amend, and change the command buttons. The real type of these is wx.aui.AuiToolBar but unless the pointer is cast correctly, you’re out of luck.

After a bunch of poking around, running in the debugger, trying to recompile (unsuccessfully) wxPython with debug symbols turned on, I stumbled upon the easy answer. You have to do this before you run any of the wx scripting apis.

import wx
import wx.aui # this is the key one

After running these, you’ll get the correct pointer types. 2

Getting a proper WxApp pointer

The second funny thing isn’t really needed to add a button, but it’s super helpful in looking at the structure of the GUI yourself. In this post, I’m not just trying to help you add a button in a specific place, but rather to help you learn more about how pcbnew is put together. With this understanding you’ll hopefully be able to do even more.

When you first open the python interpreter in pcbnew, if you ask for the wxApp pointer, you’ll get a pointer of type wxEvtHandler. While this is correct, it’s incomplete and also unhelpful if you need a wxApp pointer. If you do this, wx.GetApp() will return a usable wxApp pointer3

import wx
wx.App()

Note that if you do this more than once in a given session, it’ll crash pcbnew.4

So why do we care? Well, if you have a proper wxApp pointer you can do this (remember, only one call to wx.App() per session):

import wx
wx.App()
import wx.aui
import wx.lib.inspection
wx.lib.inspection.InspectionTool().Show()

Which will give you a Widget Inspection Tool window like the one below. It enables you to click on different parts of the pcbnew GUI and see what’s what. Note that it doesn’t give me information about the individual buttons. It’s a helpful learning/debug aid nonetheless.

So let’s add a button

There are a couple things you need to add a button

  • The toolbar you want to add the button to (top, left, or right)
  • A callback function to be called when the button is clicked
  • A picture/bitmap to use as your button’s icon.

Getting the toolbar to add to

To lookup the toolbar window, you start by finding the main pcbnew window and then asking it for one of its children by id number. How do you know the number? The current, bad, answer is to either use the window inspector I mentioned earlier in this post or for me to tell you.5 Here are the numbers:

(this has been fixed. you can now use pcbnew.ID_H_TOOLBAR, pcbnew.ID_AUX_TOOLBAR and so on.)

  • 6038 is the value that H_TOOLBAR from kicad/include/id.h happens to get. It’s the one on the top with buttons like “new board”, “open existing board”
  • 6041 is AUX_TOOLBAR. That’s the second row of stuff in the pcbnew gui. It contains things like track width, via size, grid
  • 6039 is V_TOOLBAR, the right commands window. zoom to selection, highlight net.
  • 6040 is OPT_TOOLBAR, the left commands window. disable drc, hide grid, display polar
import pcbnew
import wx
import wx.aui

def findPcbnewWindow():
    windows = wx.GetTopLevelWindows()
    pcbnew = [w for w in windows if w.GetTitle() == "Pcbnew"]
    if len(pcbnew) != 1:
        raise Exception("Cannot find pcbnew window from title matching!")
    return pcbnew[0]

pcbwin = findPcbnewWindow()
top_tb = pcbwin.FindWindowById(6038)

Callback function

This one’s easy. It’s just a function that takes the event as an argument. If you print from this function, the output will appear in the terminal used to invoke pcbnew, not the python window. The event will be of type wx.CommandEvent which is useful to know if you want to have one callback for a variety of buttons.

def MyButtonsCallback(event):
    # when called as a callback, the output of this print
    # will appear in your xterm or wherever you invoked pcbnew.
    print("got a click on my new button {}".format(str(event)))

icon image

If you were to look in the kicad source code for bitmaps_png/CMakeLists.txt you’d find this:

# Plan for three sizes of bitmaps:
# SMALL – for menus – 16 x 16
# MID – for toolbars – 26 x 26
# BIG – for program icons – 48 x 48

The rest of that file handles automagic conversion from svg files into a format that can be consumed by the kicad code. Things are easier for us. You just need a 26×26 image file. In this example, I use png format, but you can use any format listed here on the wxPython page

# assuming you're creating your button from a script, one logical place to keep
# the image is in the same directory. Here's a snippet for how to get the
# path of the script.
import inspect
import os
filename = inspect.getframeinfo(inspect.currentframe()).filename
path = os.path.dirname(os.path.abspath(filename))

# here we actually load the icon file
bm = wx.Bitmap(path + '/hello.png', wx.BITMAP_TYPE_PNG)

Putting it all together

Now we have top_tb holding the toolbar window we want to add to, bm holding a bitmap pointer, and MyButtonsCallback to do the work. Now we just need to add a new tool button, bind it to the callback and tell the system to make it real.

itemid = wx.NewId()
top_tb.AddTool(itemid, "mybutton", bm, "this is my button", wx.ITEM_NORMAL)
top_tb.Bind(wx.EVT_TOOL, MyButtonsCallback, id=itemid)
top_tb.Realize()

And that’s it. Hope you enjoyed the post. Please comment. Again, the sample code can be found here.


  1. Things are only weird if you don’t know the explanation. In this case, I can guess at the reasons, but I don’t know for sure.

  2. if you already have the pointer with the wrong type, subsequent searches for windows will yield the same unhelpful type. My theory on why this is so is a matter of caching (why it persists) and swig symbol tables. Before you do import wx.aui, these types are not yet known to python and swig, so they return a type that is known. wxControl

  3. probably incorrect, but usable for what I’m about to talk about.

  4. again, this is a bad solution, but it’s only needed for debugging and learning about pcbnew’s structure.

  5. The better answer is for the kicad enum that holds these number to be exposed in python. Easy to add, but it’s not there now.

Leave a Reply

Your email address will not be published. Required fields are marked *