Adding Custom Desktop and Taskbar Icons to you Python Executable

After going through the process of setting up pyinstaller to stop throwing false virus warnings, you probably want your fancy new desktop app to have its own custom icon.  There is a special method required for each of these icons to work as you expect.

Normal windows programs are a single .EXE file with a custom icon. When you open them, there’s a GUI and the taskbar and window icons are the same. To accomplish this, I use pySimpleGUI to design the GUI. This is simple to install and there are several version available such as pysimpleguiQt. Install this in the command line with:

pip install pysimplegui

I actually cheat a little bit here. You can do everything on the command line, but pysimplegui has a great GUI for this that simplifies things called pysimplegui-exemaker.

Step 1: Create a folder structure for your python project.

There’s likely easier ways to do this, but I typically start my projects by visiting github.com and creating a new repository on my account. Then I open github desktop and clone that directory to my computer. This gives me a correct structure for keeping track of any changes in that folder. I’ll create my main python file in this folder and edit the README file with a description and my project goals.

I create a folder to hold my assets named something like “assets” or “img” where I will save my custom icon file. This can be edited later, but I like having something here so I know from the beginning that it is set up correctly.

Step 2. Get or create a .ico file.

This is the image of your icon. They are really easy to find free ones online or you can make one from scratch using a free online editor or some image editors gimp.

Step 3. Setup Relative Filepaths

I add the following to the top of my main python script based on advice from arcade academy which allows relative filepaths for pyinstaller:

if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
os.chdir(sys._MEIPASS)

Step 4: Setting the Window Icons:

To get my custom icon in the pysimpleGUI you have to use a fill absolute path when you create your windows and all popup windows. I typically just create a global variable with the absolute path to the location of the icon.

program_icon = C:\Users\ALaptop\Documents\GitHub\myProjectFolder\assets\icon.ico
window = sg.Window('Program Title Goes Here', layout, icon =program_icon)
filename = sg.popup_get_file('Select a MPA file', no_window=True, file_types=(("myPythonApp Files", "*.mpa"), ("Text files", "*.txt"), ("All Files", "*")), icon =program_icon) 

When you run this code as a script (in VScode for example) you should see the custom icon at the top left of all the program’s windows. But your taskbar will still be a generic python icon.  You won’t see the icon in the windows taskbar until you create and run an executable using pysimplegui’s exemaker or pyinstaller. Simply running the script from the command line will NOT show the correct icon in the taskbar.

Step 5: Setting Desktop and Taskbar icons

Pyinstaller can set up a template for us to fill out for our project so it will be able to bring in all the external files we want (like icons). Simply run the following command in the command line to generate the template:

pyinstaller --onefile --noconsole .\my-Program.py
This creates a new file called “my-Program.spec” in the same folder as your code. Open this in a text editor and it’ll look something like this:
# -*- mode: python -*-

block_cipher = None

a = Analysis(['my-Program.py'],
pathex=['C:\\Users\\ALaptop\\Documents\\GitHub\\myProjectFolder'],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='my-Program',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
runtime_tmpdir=None,
console=False )

Step 6: Customizing the .spec file

Once the .spec file is created, you’ll want to edit it. In the Analysis section in datas I put the links to any assets the program will need such as images, sounds, etc. If I only have images in a subfolder named “img” it would be

datas=[( 'img/*png', 'img' )],

The first parenthesis is where the files are currently stored, and the second is the name of the folder inside the temporary folder that pyinstaller uses when the exe runs. Remember how I said Pyinstaller creates a self-extracting zip file? Here’s where that comes into play. You are telling pyinstaller where you stored your assets relative to your project’s folder, and then telling it where it can put these files later. This path is important especially if you used relative links in your python code. If your EXE can’t find the files it needs, it’ll crash giving you a popup stating “Failed to execute script my-Program”. This can be confusing to debug if you don’t know how to see what the executable is looking for. Also, this expects that you pasted the lines from Step 3 above into your code which sets up relative filepaths.

To understand and to debug this, you have to remember that the EXE pyinstaller creates is a zip file that is made up of a bootloader (which can actually run your python code), your python script, any libraries your script uses auto extracts a folder, and any other files needed to run your script (images, audio, etc). When you run the executable, it actually decompresses all this and sticks it in a folder in your C:\Users\<username>\AppData\Local\Temp in a directory named “_MEIxxxxx” where the ‘x’s are a random number. This folder exists only while the executable (or the error popup) is opened. As soon as you close it, this folder is deleted. You need to visit this folder to debug issues. To debug, you’ll need to pay close attention to the paths in the spec file and your code’s paths and then comparing them to the location of assets inside this temporary _MEIxxxxx folder.

For instance, in the datas entry above, the second set of quotes denotes the name of the folder INSIDE the temporary folder. That’s where pyinstaller is placing the PNGs I refer to in that line. If my app keeps crashing for some reason, I can make sure that these files exist in the correct folder in the C:\Users\<username>\AppData\Local\Temp\_MEIxxxxx\img folder.

Add the absolute path to your custom icon to the .spec file. This is added to the “EXE” section as part of the list of settings. I also added “console=False” so it would not open a console while executing.

console=False , icon='C:\\Users\\ALaptop\\Documents\\GitHub\\myProjectFolder\img\\icon.ico')

You can see my full spec file below. Once you’ve saved the spec file changes, you will issue the same command as before, but use the .spec file instead of the python file.

pyinstaller --onefile .\my-Program.spec

Just for good measure, I also put the fullpath in the pysintaller command as well sometimes:

pyinstaller --onefile --noconsole --icon "C:\\Users\\ALaptop\\Documents\\GitHub\\myProjectFolder\\icon.ico" --add-data "*png;." .\my-Program.spec

Here’s the full final .spec file I would use:

 # -*- mode: python -*-

block_cipher = None


a = Analysis(['my-Program.py'],
             pathex=['C:\\Users\\ALaptop\\Documents\\GitHub\\myProjectFolder'],
             binaries=[],
             datas=[( 'img/*png', 'img' )],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)

             
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          [],
          name='my-Program',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          runtime_tmpdir=None,
          console=False , icon='C:\\Users\\ALaptop\\Documents\\GitHub\\myProjectFolder\\img\\icon.ico')

 

Program Icon (Desktop Icon):

This will be the icon users see and doubleclick on their desktops to open your app.

After running pysimplegui’s exemaker one time for your project as normal, it will create a .spec file template for you.  Once the .spec file is created, you have to put the absolute path to the icon you want to use in that .spec file. For instance, I had my icon in a folder called “img” inside my python’s project folder. Paste the path at the bottom of the EXE section like this:

console=False , icon='C:\\Users\\ALaptop\\Documents\\GitHub\\myPythonApp\\img\\icon.ico')

Just for good measure, I also put the fullpath in the pysintaller command as well:

pyinstaller --onefile --noconsole --icon "C:\\Users\\ALaptop\\Documents\\GitHub\\myPythonApp\\icon.ico" --add-data "*png;." .\myPythonApp.spec

Window and Taskbar Icon:

This is set in pysimgplegui when you create any window (even popups can be given a different icons). The only way this works is if you code the full absolute path in the python script, but you won’t see it in the taskbar until you create and run an executable using pysimplegui’s exemaker or pyinstaller. Simply running the script from the command line will NOT show the correct icon in the taskbar.

program_icon = 'C:/Users/ALaptop/Documents/GitHub/myPythonApp/img/icon.ico'

filename = sg.popup_get_file('Select a MPA file', no_window=True, file_types=(("myPythonApp Files", "*.mpa"), ("Text files", "*.txt"), ("All Files", "*")), icon =program_icon)

 

 

RGB565 to RGB888 Color Conversion

While working with Charlotte Latin FabLab students this year, we came across an issue. Alex was using a camera module for her Arduino project (ArduCam) that gave RGB565 formatted pixel data but she needed to convert it to RGB888. Though there are tons of posts on stackExchange about how to do it, I couldn’t find a simplified broken-down explanation of how it works to point her to, so I decided to write one.

RGB565 means there are 5-bits of data for the Red component of the pixel, 6-bits for green and 5-bits for the blue.  It looks like this:
pixel value =
RED= R4, R3, R2, R1, R0,
GREEN= G5, G4, G3, G2, G1, G0,  
BLUE= B4, B3, B2, B1, B0

But Remember RGB565 is stored as two separate values (VL and VH from the example code for the camera we used), with the low value on the left and high value on the right.
VL = G2, G1, G0, B4, B3, B2, B1, B0     VH = R4, R3, R2, R1, R0, G5, G4, G3

Some clever bitmasking can strip out each component color and place them into individual variables. We’ll AND the bits of the values we want from VL and VH with ‘1’ or ‘0’ depending on which we want at a given time.

Of course this has to be done for each and every pixel, and she had 320 x 240 in her original image.

This is a simple nested for loop that already used in the example code to read the data from the camera

for (int i=0; i<240; i++){

  
   for (int j=0; j<320; j++){
  
    //Convert the RGB 565 to RGB 888 of the pixel in the bitmap and store it as PIXEL data type in the new larger array.

  
    //Step A convert to the new PIXELS format

  
    ///Step B scale from 565 to 888

  
    }//end for j

  
 }//end for i

STEP A: Convert the color to a new pixel format.

Based on some example code, it’s best to create a structure to contain the R, G and B values:

typedef struct { 
unsigned char blue; 
unsigned char green; 
unsigned char red; 
} PIXELS; 

Then we need a large array to store the pixels of the new image:

PIXELS bigPIXELS [12] [12]; //the resulting RGB888 picture pixels are stored here

Then you can convert each pixel by using logic operations and bit shifting.

/*Blue is the easiest to see how this works so we'll do it first.
  
 The values for Blue is stored in VL So for example if VL = 1011  0101( blue = 21 in decimal) 
 to get the blue values all alone, we can AND them with 1s and AND the green bits with 0s 
 leaving you with an 8-bit number with only the blue data in the right spot. The result 
 is: 0001  0101  or 21 in decimal. 
*/
  bigPIXELS [i][j].blue =    ( VL & 0b00011111 ) ; //0x1F
 
 

/*Red is only slightly more complicated. We first get bits for red alone, then shift them 
 to the decimal point The 0b11111000 gives you only red data as before, but the digits 
 aren't in the right place. 
 
 There are place-holding 0s that make the value of red too big: 
 For example is VH = 1101  0110  The value of red is 26 in decimal here. ANDing as before
 would give you only 1101  0000, but if we convert this number to decimal, it =208, which 
 isn't right at all. Shifting to the right by 3 (>>3) moves the red value down to the 
 decimal point and gives you 0001  1010 = 26 in decimal in 8-bits
*/
  
bigPIXELS [i][j].red =   ((VH & 0b11111000) >> 3); //0xF8
 

/*Green is the most complicated as it has components in each VL and VH so we'll AND the 
 bits with 1s and then shift them to the right places, then we need to OR them to the correct spot
  
 VL = G2, G1, G0, B4, B3, B2, B1, B0  so to get only green values
 */ 

// since we don't use this variable for anything else, we can just dump the result of this operation back into VL
 VL&= 0b11100000 ;  // 0xE0  

 // VH = R4, R3, R2, R1, R0, G5, G4, G3  so to get only green values  
 VH &= 0b00000111; //0x07
  
 //Now shift the bits to the right places and OR them together to glue them into one number
bigPIXELS [i][j].green =  (uint8_t(VL) >> 5) | (VH << 3); //Shift lower 3 bits in VL to the decimal point, and shift the upper 3-bits in VH to their correct positions.

Great! Now we have our RGB values by themselves, now let’s let them stretch their arms to take up the full 8-bits that’s now available to them.

Step B: Converting from one colorspace to another is simply a function of scaling. You’ve already done this a lot actually. Any time you’ve read an analog input, then used that to control an analog output on arduino you’ve done it. Imagine a knob on the Arduino’s analog input. Analog inputs can read values from 0-1023 (10-bits) where 100% is = 1023. Imagine a single red pixel as an LED on an analog output. It can only take values between 0 and 255 (8-bits) where 255 is 100% brightness of the LED.

You can do the same thing here. In a 5-bit number system, 100% is when all bits are 1’s so it is 11111. This needs to be converted to an 8-bit system where 100% is = 1111  1111.

You use Arduino’s map() function to map the values from one scale(0-1023) to the other (the LED’s 0-255), however this is very inefficient and slow.

Since you know what the numbers are going to be for the scaling calculation, and they won’t ever change you can pre-calculate them for your code. You’re scaling from 5-bits to 8-bits for red and blue which is 0xFF / 0x1F = 255 / 31 = 8. For green you start with 6-bits and convert to 8 which is 0xFF / 0x03 = 255 / 63 = 4.  You could multiply these hardcoded numbers to scale red *= 8; green *=4; blue*=8; however since these are powers of 2, there’s a trick to make this code faster. Simply bit shift left. For each bit you shift, it is equal to a power of two so the code would be red and blue << 3 and green << by 2.

//If you want to see the RGB 565 values print them here, or comment out if you don't need them
Serial.print(" RGB565 =");
Serial.write(bigPIXELS [i][j].red);
Serial.write(bigPIXELS [i][j].green);
Serial.write(bigPIXELS [i][j].blue);

bigPIXELS[i][j].blue = bigPIXELS[i][j].blue << 3;//blue and red were 5-bits

bigPIXELS[i][j].red=  bigPIXELS[i][j].red<<3;

bigPIXELS[i][j].green =  bigPIXELS[i][j].green <<2;//Remember that green has 6 pixels to begin with so it only shifts 2 places

Now you can do with these as you please. Add a BMP header and print them to the serial port to be able to view this BMP image on your computer.

Serial.print(" RGB888 R= ");
Serial.write(bigPIXELS [i][j].red);
Serial.write(bigPIXELS [i][j].green);
Serial.write(bigPIXELS [i][j].blue);