Outils pour utilisateurs

Outils du site


issue182:python

Ceci est une ancienne révision du document !


Being from Texas, most of us here have a tendency to “change horses in midstream” as the saying goes, and this month, I’m afraid I’m going to have to do just that. I was planning to continue my article from last month (FCM 181) on understanding styles and themes for ttk and Tkinter, and in fact started down that path. However, in working with a user of PAGE for the last few days, trying to solve a problem, I realized that there is an issue out there, lurking and waiting to bite every programmer in their proverbial backsides who uses images and Tkinter in their program creation process. That issue is that we, as programmers, do not know in advance where our end users will run our programs from. Will it be from the Desktop, which is becoming more and more popular? Will it be from the folder that contains the source code? Even if we state how the program should be run, that doesn’t mean that our ultimate end users will pay attention to our requests/demands, and if things don’t fit their expectations, watch out for the complaints. So, in order to try to help us all be mindful of this pending issue, I’ve decided to press pause for a month and continue our discussion on Styles and Themes next month.

So what brought up this “issue”? Well, the user was trying to use a demo program that I wrote that “shows off” just a few of the capabilities of the widgets that PAGE supports. These can be broken down into 3 groups. There are the “standard” Tk widgets like standard buttons, labels and so on. Then there are the ttk Themed widgets, like the TButton, TLabel, Treeview, TCombobox, etc. Finally there are the “enhanced” widgets that usually have scrollbars built in, like the ScrolledListbox or the ScrolledText widgets. More times than not, these “enhanced” widgets are based on the “standard” Tk widgets, but a handful are based on a ttk widget. Widgets like Tk Radiobuttons and Checkbuttons have a way to change the look of how the widget appears when the program is run, which can involve using custom graphic images, one for the On state and one for the Off state, in addition to a few extra attribute settings. This allows a “normally ugly and looks like Windows 95” Tkinter program to look clean and fresh and receives a bunch of “Oohs” and “Ahhs” from the end users (and other programmers as well). So when we want to use graphics in our Tkinter programs, we have to be mindful of the location where the program will “live”, and where the ultimate end user will attempt to execute our program from.

I had run into this a long time ago when I created a program for my own use, never really expecting to share it with anyone. The program uses a large number of graphic files, all .png files which are located in a sub-folder of the main source code. When the program was executed directly from the source code folder, everything worked just fine. When I tried to create a link from the Desktop that would call Python with the full path of the source code, it failed. I finally tracked it down to the path to the graphic files “couldn’t be found”. I tried a number of solutions without success until I stumbled upon a somewhat messy fix, which was to create a complete and fully qualified path which started with “/home/greg/…”. Of course, I hardcoded this in my early development, to save time and energy and eventual hair loss from my pulling them out in great handfuls. I went on with my development, not really thinking of the fact that by doing it that way, not only could I not share the program with anyone, but I couldn’t even move the program from its hard-coded location. My mind, at the time, was focused on getting the thing to work, not the sloppy programming methods I was employing. Other things came up and development of this program, which worked for the limited things I wanted to accomplish, was shelved and all of what I had learned went on that shelf as well.

Jumping back to the current issue, this was one of the problems that was plaguing this user when he tried to run the demo on his Raspberry Pi from his desktop. There was one graphic in the program (while there were a number of other graphics there as well), that was causing the program to fail for him. Actually there were other images that were also a problem, but this was the first one that the program tried to load. After a long troubleshooting session, I finally realized what was going on and asked the user to try to run the program from the source code folder. He had some other issues that prevented it, but when push came to shove, it would run correctly.

Trying to remember what the ticklers in my old brain were trying to tell me about the past lessons learned, I attempted to come up with a properly “pythonic” method to dynamically set a path statement that would keep everything happy, no matter where the program was started from, and no matter where it existed. Digging into the dusty resources that I had here and out on the Internet about the issue, I threw together a very quick and VERY dirty demonstration that not only showed the issue but presented some helpful information (albeit redundant in nature) to someone wondering about a quick but clean method around the problem.

The Tool

Since I was trying to create a GUI tool to easily point out the issue, of course I created a quick GUI in PAGE, not really trying to adhere to good GUI creation concepts.

I decided on 6 different (but very similar) Python library calls to help determine the information that will need to be presented in order to run a Python script not only from the source code folder, but also from /home and /Desktop, and wherever else anyone could possibly think of to attempt to run the program.

Of course, the GUI would need to provide a quick graphic representation showing either the success or failure of loading an image into a Tkinter widget as well as a quick indication of what each of the 6 different function calls returned, so I (or anyone else) could quickly decide on a solution.

When run directly from the source code folder, you can see that no matter which of the 6 system calls I made, they all returned the same information, which, on many levels, was quite a comfort for me. However, when the program is run from /home or /Desktop, the difference is shown.

Three of the system calls ended up returning the same information, and the other three returned a different result, but all three of those were consistent in their returned data.

Here (previous page, top right) is a quick look and description of each. Honestly there are dozens more ways out there to do this, so if you want to try others, feel free. I’m just here to get the thought processes flowing.

So there are 4 functions from the os.path library and 2 from the pathlib library. When the program is run from a location other than the source folder, the three that return just the location of where the program is run from are:

os.path.abspath(filename)

pathlib.Path().absolute()

os.path.abspath(os.getcwd())

This means that for this specific use case, these three system calls can’t be used for our purposes.

Of the remaining three choices, any one of them returns the full path to our source folder, which is what we really want. We can always assign this to a variable for the path and append the path to our local image folder and the filename.

The Code

Since this is a PAGE program and a very simple one at that, I decided to put all of the code in a function called “startup” which will get run just before the GUI is actually shown to the user. This includes trying to load both of the images as well as obtaining the path from the 6 system calls and loading a Text widget and the two dynamic text labels. The call to the startup function is the next-to-last line in the main function that PAGE provides.

Of course, we have to import the os and pathlib libraries into our program.

def startup():

  import os, pathlib

At this point, I assign a variable name (somewhat explicit to what it is) from each of the 6 system calls, and then print the value of each of the variables using a f-string formatted string (top right).

After looking at the results when I ran it from /Desktop, I decided on using these two variables to provide what the program thinks is the proper information (localpath and abspath1). The localpath variable (localpath = os.path.dirname(os.path.abspath(file))) is the information we will eventually use. But I’m getting ahead of myself and have spoiled the surprise. Anyway, I then use the .set() method of the two text labels in the form.

# Load the two labels with the paths

_w1.LocalPath.set(localpath)

_w1.ProperPath.set(abspath1)

At this point, I can define the image name, which includes the path (located from the source directory) and filename.

# define the image name

imgname = '/images/icons/document.png'

Now, we can try to load the images into the two Label widgets that we use to show the graphics. I say “try” since I know that if the program is run from /Desktop, it will fail. We’ll trap that failure and show a messagebox with an error, just to be kind (code shown next page, top right).

Please notice that I didn’t worry about any possible errors with the “localpath” , since it’s a fully qualified path and as long as: • I type in the correct graphic filename and • The graphic exists, then there won’t be a problem loading that graphic. I know that I’m being overly optimistic, but that’s just me.

The last thing I do is load the ScrolledText widget with the resulting outputs of each of the variables. We have to use the .insert() method, with the position that we want to add the text and then the data we want to insert. Since each line ends with a “\n”, and since the Text widget remembers the last place something was placed, it’s just an easy job to use the Tk.END (PAGE now imports the Tk.Constants module) to define the “where”, and the f-string formatted data as the “what”, and let Tkinter deal with the “how” (shown bottom left).

That’s it. So the bottom line is: if you are going to combine Tkinter and Graphics and Python, you probably should define the fully qualified path as the output from a call to os.path.dirname(os.path.abspath(file)) .

Quick Update

I did a quick writeup to show the results to the user as well as Don (The author of PAGE). Don responded quickly with a fix in the form of a new alpha version of PAGE for me to test. The result of this new cut provides yet another option for PAGE users (and can be used by other Python programmers as well. In PAGE, any embedded graphics included at design time are handled in the GUI file. His fix looks something like this…

_script = sys.argv[0]

_location = os.path.dirname(_script)

Since this is a global variable by default, the _location variable is available to the rest of the project as projectname._location. This makes it simple to handle images within the _support module. If you wish to create a simple global for an image path in your project, you can do something like this…

  location = test1._location
  global ImageDir
  ImageDir = os.path.join(location, "images", "icons")

Then when you need to assign an image, you can use a simple definition anywhere within the project _support file.

my_file = os.path.join(ImageDir, “folder.png”)

This is not limited to PAGE or images. Assume you want to use a database in your Python script. You must point the program at the database file. By using this method, it gives you a quick way to set the path to the database without worrying about where the user is running your program from (image is shown on the next page, top left).

I have added my project and code to my github repository at https://github.com/gregwa1953/FCM-182-Python .

I promise I will REALLY try to continue the Tkinter Styles and Themes discussion next month!

Until next time, as always; stay safe, healthy, positive and creative!

issue182/python.1656314538.txt.gz · Dernière modification : 2022/06/27 09:22 de auntiee