I would like to be able to save my session state within the PythonWin editor (e.g. these three files are opened and positioned in these particular locations within the PythonWin window). I can get handles to each of the child windows within PythonWin using win32gui, as well as the titles of each of the files and the positions/sizes of the windows. I'm unclear though in how to get the full path for the file listed as the child window name (i.e. if child window name is test.py and test.py lives at c:\python\test.py, I don't know how to get c:\python). I was thinking I would write out which files were opened plus their window positions to a small file that I would then call at PythonWin start time for loading.
Any ideas on how to get the full paths to the child window names?
Alternatively if someone already has a more elegant solution for saving session state in PythonWin please pass it along.
Below is the code I'm using right now (thanks to Michal Niklas for the starter code for using win32gui).
import win32gui
import re
MAIN_HWND = 0
def is_win_ok(hwnd, starttext):
s = win32gui.GetWindowText(hwnd)
if s.startswith(starttext):
global MAIN_HWND
MAIN_HWND = hwnd
return None
return 1
def find_main_window(starttxt):
global MAIN_HWND
win32gui.EnumChildWindows(0, is_win_ok, starttxt)
return MAIN_HWND
def winPos(hwnd):
if type(hwnd) == type(1): ( left, top, right, bottom ) = win32gui.GetWindowRect(hwnd)
return "%i, %i, %i, %i" % (left, right, top, bottom)
def winName(hwnd, children):
s = win32gui.GetWindowText(hwnd)
rePy = re.compile(r'[a-zA-Z1-9_ ]*.py')
rePySearch = rePy.search(s)
if rePySearch is not None:
if rePySearch.group()[0:7] != "Running":
s = s + ',' + winPos(hwnd) + '\n'
children.append(s)
return 1
def main():
children = []
main_app = 'PythonWin'
hwnd = win32gui.FindWindow(None, main_app)
if hwnd < 1:
hwnd = find_main_window(main_app)
if hwnd:
win32gui.EnumChildWindows(hwnd, winName, children)
filename = "sessionInfo.txt"
sessionFile = os.path.join(sys.path[0],filename)
fp=open(sessionFile, 'wb')
for i in range(len(children)):
fp.write(children[i])
fp.close()
main()
I could be wrong, but isn't PythonWin written in Python?
Have you tried reading the source to the "Save" command to figure out where it stores its full paths?
(I'd take a look myself, but I haven't used Windows in half a decade)
Related
When a "choose files" dialog is displayed I want to pre-select files in a project which are already configured as being "part of" that project, so the user can select new files OR unselect existing (i.e. previously chosen) files.
This answer suggests multiple selection should be possible.
For this MRE, please make 3 files and put them in a suitable ref_dir:
from PyQt5 import QtWidgets
import sys
class Window(QtWidgets.QWidget):
def __init__(self):
super(Window, self).__init__()
self.button = QtWidgets.QPushButton('Test', self)
self.button.clicked.connect(self.handle_button)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.button)
def handle_button(self):
options = QtWidgets.QFileDialog.Options()
options |= QtWidgets.QFileDialog.DontUseNativeDialog
ref_dir = 'D:\\temp'
files_list = ['file1.txt', 'file2.txt', 'file3.txt']
fd = QtWidgets.QFileDialog(None, 'Choose project files', ref_dir, '(*.txt)')
fd.setFileMode(QtWidgets.QFileDialog.ExistingFiles)
fd.setOptions(options)
# fd.setVisible(True)
for file in files_list:
print(f'selecting file |{file}|')
fd.selectFile(file)
string_list = fd.exec()
print(f'string list {string_list}')
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
Unfortunately, despite ExistingFiles having been chosen as the file mode, I find that it is only the last file selected which has the selection... but I want all three to be selected when the dialog is displayed.
I tried experimenting with setVisible to see whether the multiple selection could be achieved somehow after the dialog is displayed, but this didn't work.
Since a non-native file dialog is being used, we can access its child widgets to control its behavior.
At first I thought about using the selection model of the item views, but this won't update the line edit, which is responsible of checking if the files exist and enabling the Ok button in that case; considering this, the obvious solution is to directly update the line edit instead:
def handle_button(self):
# ...
existing = []
for file in files_list:
if fd.directory().exists(file):
existing.append('"{}"'.format(file))
lineEdit = fd.findChild(QtWidgets.QLineEdit, 'fileNameEdit')
lineEdit.setText(' '.join(existing))
if fd.exec():
print('string list {}'.format(fd.selectedFiles()))
The only drawback of this approach is that the fileSelected and filesSelected signals are not sent.
Musicamante's answer was very, very helpful, in particular showing that the selection is in fact triggered by filling the QLE with path strings.
But in fact there is a fatal flaw when the purpose is as I have stated: unfortunately, if you try to deselect the final selected file in a directory, actually this name is not then removed from the QLE. And in fact, if the QLE is set to blank this disables the "Choose" button. All this is by design: the function of a QFileDialog is either to "open" or to "save", not to "modify".
But I did find a solution, which involves finding the QListView which lists the files in the directory, and then using a signal on its selection model.
Another thing this caters for is what happens when you change directory: obviously, you then want the selection to be updated on the basis of the project's files as found (or not found) inside that directory. I've in fact changed the text of the "choose" button to show that "modification" is the name of the game.
fd = QtWidgets.QFileDialog(app.get_main_window(), 'Modify project files', start_directory, '(*.docx)')
fd.setFileMode(QtWidgets.QFileDialog.ExistingFiles)
fd.setViewMode(QtWidgets.QFileDialog.List)
fd.setLabelText(QtWidgets.QFileDialog.Reject, '&Cancel')
fd.setLabelText(QtWidgets.QFileDialog.Accept, '&Modify')
fd.setOptions(options)
file_name_line_edit = fd.findChild(QtWidgets.QLineEdit, 'fileNameEdit')
list_view = fd.findChild(QtWidgets.QListView, 'listView')
# utility to cope with all permutations of backslashes and forward slashes in path strings:
def split_file_path_str(path_str):
dir_path_str, filename = ntpath.split(path_str)
return dir_path_str, (filename or ntpath.basename(dir_path_str))
fd.displayed_dir = None
sel_model = list_view.selectionModel()
def sel_changed():
if not fd.displayed_dir:
return
selected_file_paths_in_shown_dir = []
sel_col_0s = sel_model.selectedRows()
for sel_col_0 in sel_col_0s:
file_path_str = os.path.join(fd.displayed_dir, sel_col_0.data())
selected_file_paths_in_shown_dir.append(file_path_str)
already_included = file_path_str in self.files_list
if not already_included:
fd.project_files_in_shown_dir.append(file_path_str)
# now find if there are any project files which are now NOT selected
for project_file_path_str in fd.project_files_in_shown_dir:
if project_file_path_str not in selected_file_paths_in_shown_dir:
fd.project_files_in_shown_dir.remove(project_file_path_str)
sel_model.selectionChanged.connect(sel_changed)
def file_dlg_dir_entered(displayed_dir):
displayed_dir = os.path.normpath(displayed_dir)
# this is set to None to prevent unwanted selection processing triggered by setText(...) below
fd.displayed_dir = None
fd.project_files_in_shown_dir = []
existing = []
for file_path_str in self.files_list:
dir_path_str, filename = split_file_path_str(file_path_str)
if dir_path_str == displayed_dir:
existing.append(f'"{file_path_str}"')
fd.project_files_in_shown_dir.append(file_path_str)
file_name_line_edit.setText(' '.join(existing))
fd.displayed_dir = displayed_dir
fd.directoryEntered.connect(file_dlg_dir_entered)
# set the initially displayed directory...
file_dlg_dir_entered(start_directory)
if fd.exec():
# for each file, if not present in self.files_list, add to files list and make self dirty
for project_file_in_shown_dir in fd.project_files_in_shown_dir:
if project_file_in_shown_dir not in self.files_list:
self.files_list.append(project_file_in_shown_dir)
# also add to list widget...
app.get_main_window().ui.files_list.addItem(project_file_in_shown_dir)
if not self.is_dirty():
self.toggle_dirty()
# but we also have to make sure that a file has not been UNselected...
docx_files_in_start_dir = [f for f in os.listdir(fd.displayed_dir) if os.path.isfile(os.path.join(fd.displayed_dir, f)) and os.path.splitext(f)[1] == '.docx' ]
for docx_file_in_start_dir in docx_files_in_start_dir:
docx_file_path_str = os.path.join(fd.displayed_dir, docx_file_in_start_dir)
if docx_file_path_str in self.files_list and docx_file_path_str not in fd.project_files_in_shown_dir:
self.files_list.remove(docx_file_path_str)
list_widget = app.get_main_window().ui.files_list
item_for_removal = list_widget.findItems(docx_file_path_str, QtCore.Qt.MatchExactly)[0]
list_widget.takeItem(list_widget.row(item_for_removal))
if not self.is_dirty():
self.toggle_dirty()
After a few hours of playing around, here's a pretty good way to programatically pre-select multiple files in a QFileDialog:
from pathlib import Path
from PyQt5.QtCore import QItemSelectionModel
from PyQt5.QtWidgets import QFileDialog, QListView
p_files = Path('/path/to/your/files')
dlg = QFileDialog(
directory=str(p_files),
options=QFileDialog.DontUseNativeDialog)
# get QListView which controls item selection
file_view = dlg.findChild(QListView, 'listView')
# filter files which we want to select based on any condition (eg only .txt files)
# anything will work here as long as you get a list of Path objects or just str filepaths
sel_files = [p for p in p_files.iterdir() if p.suffix == '.txt']
# get selection model (QItemSelectionModel)
sel_model = file_view.selectionModel()
for p in sel_files:
# get idx (QModelIndex) from model() (QFileSystemModel) using str of Path obj
idx = sel_model.model().index(str(p))
# set the active selection using each QModelIndex
# IMPORTANT - need to include the selection type
# see dir(QItemSelectionModel) for all options
sel_model.select(idx, QItemSelectionModel.Select | QItemSelectionModel.Rows)
dlg.exec_()
dlg.selectedFiles()
>>> ['list.txt', 'of.txt', 'selected.txt', 'files.txt']
I want to automate the testing of a GUI. In particular I want to test the "save" option from the file menu. I.e. when the save button is clicked the data from the fields in the UI are collected and then written to a json file.
The problem is, when the QFileDialog pops up asking for the user to enter the name of the file to save to, I cant get a handle on the dialog to continue testing. How do you automate this bit? I am not sure how to get a handle on the QFileDialog.
def hdl_save_report_as(self):
try:
self.dialog.setDefaultSuffix('json')
save_file, _ = self.dialog.getSaveFileName(caption="Save Report", filter="JSON Files (*.json)")
if save_file:
score = self.parent.main_tab_view.get_fields()
self.ui_model.report_path = save_file
with open(save_file, 'w') as f:
json.dump(score, f, indent=4)
except Exception as e:
result = dlg.message_dialog("Exception", "We ran into an error!", QMessageBox.Warning, e)
print(e)
def test_save_report(self):
self.main_window.menu.bt_save_file.trigger()
self.main_window.menu.dialog.selectFile('a_test_report.json')
## save_dialog = QApplication.activeModalWidget()
# save_dialog = QApplication.activeWindow()
# children = save_dialog.findChildren()
# active_line_edit = None
# confirm_button = None
# for c in children:
# if type(c) is QLineEdit:
# if c.hasFocus():
# active_line_edit = QLineEdit(c)
# # if type(c) is QPushButton:
# # if
# active_line_edit.setText("a_test_report")
self.assertTrue(os.path.exists(os.path.join(os.getcwd(), 'a_test_report.json')))
I have tried a few different approaches, is there a standard way to do this? Hopefully I have missed something obvious.
From what I can see and as explained in another post it is not possible to simulate interaction with the special QFileDialogs like getSaveFileName or getOpenFileName.
My approach is now managing the files in setup function like
def setUp(self):
self.test_report = "a_test_report.json"
# Remove an existing test report
if os.path.exists(self.test_report):
os.remove(self.test_report)
self.assertFalse(os.path.exists(os.path.join(os.getcwd(), self.test_report)))
# Create a file to test reading / writing
with open(self.test_report, 'w') as f:
f.write("")
self.assertTrue(os.path.exists(os.path.join(os.getcwd(), self.test_report)))
self.assertTrue(os.stat(self.test_report).st_size == 0)
self.main_window = MainWindow()
And testing like
def test_save_action(self):
# Trigger the save button - check if the file was overwritten with new info
self.main_window.menu.bt_save_file.trigger()
self.assertTrue(os.stat(self.test_report).st_size > 0)
Although it seems it is not possible to test a "Save As" function.
First of all, I'm a newby in python. Had to take a course of it in college and got hooked by its efficiency.
I have this sticky problem where the Windows 7 prompt becomes unresponsive after using a curses window. In Windows 10 it works well. Note that I'm using the Win7 terminal with its default settings. In my code I create a curses window to show 2 simultaneous progress bars, each for a file download. I implemented this by passing the curses window to a FileDownload class (one class instance for each download) that handles its progress bar inside this window. Oddly, in Windows 7 when the downloads are done and the control returns to the prompt, it becomes unresponsive to the keyboard. I worked around this by invoking curses.endwin() after using the window, but this causes the prompt to display all the way down the screen buffer, what hides the curses window.
Here is my code. Any ideas are greatly appreciated. Thanks!
# Skeleton version for simulations.
# Downloads 2 files simultaneously and shows a progress bar for each.
# Each file download is a FileDownload object that interacts with a
# common curses window passed as an argument.
import requests, math, threading, curses, datetime
class FileDownload:
def __init__(self, y_pos, window, url):
# Y position of the progress bar in the download queue window.
self.__bar_pos = int(y_pos)
self.__progress_window = window
self.__download_url = url
# Status of the file download object.
self.__status = "queued"
t = threading.Thread(target=self.__file_downloader)
t.start()
# Downloads selected file and handles its progress bar.
def __file_downloader(self):
file = requests.get(self.__download_url, stream=True)
self.__status = "downloading"
self.__progress_window.addstr(self.__bar_pos + 1, 1, "0%" + " " * 60 + "100%")
size = int(file.headers.get('content-length'))
win_prompt = "Downloading " + format(size, ",d") + " Bytes:"
self.__progress_window.addstr(self.__bar_pos, 1, win_prompt)
file_name = str(datetime.datetime.now().strftime("%Y-%m-%d_%H.%M.%d"))
dump = open(file_name, "wb")
# Progress bar length.
bar_space = 58
# Same as an index.
current_iteration = 0
# Beginning position of the progress bar.
progress_position = 4
# How many iterations will be needed (in chunks of 1 MB).
iterations = math.ceil(size / 1024 ** 2)
# Downloads the file in 1MB chunks.
for block in file.iter_content(1024 ** 2):
dump.write(block)
# Progress bar controller.
current_iteration += 1
step = math.floor(bar_space / iterations)
if current_iteration > 1:
progress_position += step
if current_iteration == iterations:
step = bar_space - step * (current_iteration - 1)
# Updates the progress bar.
self.__progress_window.addstr(self.__bar_pos + 1, progress_position,
"#" * step)
dump.close()
self.__status = "downloaded"
# Returns the current status of the file download ("queued", "downloading" or
# "downloaded").
def get_status(self):
return self.__status
# Instantiates each file download.
def files_downloader():
# Creates curses window.
curses.initscr()
win = curses.newwin(8, 70)
win.border(0)
win.immedok(True)
# Download URLs.
urls = ["http://ipv4.download.thinkbroadband.com/10MB.zip",
"http://ipv4.download.thinkbroadband.com/5MB.zip"]
downloads_dct = {}
for n in range(len(urls)):
# Progress bar position in the window for the file.
y_pos = n * 4 + 1
downloads_dct[n + 1] = FileDownload(y_pos, win, urls[n])
# Waits for all files to be downloaded before passing control of the terminal
# to the user.
all_downloaded = False
while not all_downloaded:
all_downloaded = True
for key, file_download in downloads_dct.items():
if file_download.get_status() != "downloaded":
all_downloaded = False
# Prevents the prompt from returning inside the curses window.
win.addstr(7, 1, "-")
# This solves the unresponsive prompt issue but hides the curses window if the screen buffer
# is higher than the window size.
# curses.endwin()
while input("\nEnter to continue: ") == "":
files_downloader()
Perhaps you're using cygwin (and ncurses): ncurses (like any other curses implementation) changes the terminal I/O mode when it is running. The changes that you probably are seeing is that
input characters are not echoed
you have to type controlJ to end an input line, rather than just Enter
output is not flushed automatically at the end of each line
It makes those changes to allow it to read single characters and also to use the terminal more efficiently.
To change back to the terminal's normal I/O mode, you would use the endwin function. The reset_shell_mode function also would be useful.
Further reading:
endwin (ncurses manual)
reset_shell_mode (ncurses manual)
I'm fairly new to Python. I'm trying to input a file name (complete with full path) to a TKinter entry widget.
Since the path to the file name can be very long I would like to be able to drag and drop the file directly
from Windows Explorer. In Perl I have seen the following:
use Tk::DropSite;
.
.
my $mw = new MainWindow;
$top = $mw->Toplevel;
$label_entry = $top->Entry(-width => '45',. -background => 'ivory2')->pack();
$label_entry->DropSite(-dropcommand => \&drop,-droptypes => 'Win32',);
Is there something similar I can do using TKinter in Python?
Tk does not have any command to handle that, and Python doesn't include any extra Tk extension to perform drag & drop inter-applications, therefore you need an extension to perform the operation. Tkdnd (the Tk extension at http://sourceforge.net/projects/tkdnd, not the Tkdnd.py module) works for me. To use it from Python, a wrapper is required. Quickly searching for one, it seems http://mail.python.org/pipermail/tkinter-discuss/2005-July/000476.html contains such code. I did another one because I didn't like that other one. The problem with my wrapper is that it is highly untested, in fact I only used the function bindtarget and only for 10 seconds or so.
With the wrapper below, you can create some widget and announce that it supports receiving dragged files. Here is one example:
# The next two lines are not necessary if you installed TkDnd
# in a proper place.
import os
os.environ['TKDND_LIBRARY'] = DIRECTORYTOTHETKDNDBINARY
import Tkinter
from untested_tkdnd_wrapper import TkDND
root = Tkinter.Tk()
dnd = TkDND(root)
entry = Tkinter.Entry()
entry.pack()
def handle(event):
event.widget.insert(0, event.data)
dnd.bindtarget(entry, handle, 'text/uri-list')
root.mainloop()
And here is the code for untested_tkdnd_wrapper.py:
import os
import Tkinter
def _load_tkdnd(master):
tkdndlib = os.environ.get('TKDND_LIBRARY')
if tkdndlib:
master.tk.eval('global auto_path; lappend auto_path {%s}' % tkdndlib)
master.tk.eval('package require tkdnd')
master._tkdnd_loaded = True
class TkDND(object):
def __init__(self, master):
if not getattr(master, '_tkdnd_loaded', False):
_load_tkdnd(master)
self.master = master
self.tk = master.tk
# Available pre-defined values for the 'dndtype' parameter:
# text/plain
# text/plain;charset=UTF-8
# text/uri-list
def bindtarget(self, window, callback, dndtype, event='<Drop>', priority=50):
cmd = self._prepare_tkdnd_func(callback)
return self.tk.call('dnd', 'bindtarget', window, dndtype, event,
cmd, priority)
def bindtarget_query(self, window, dndtype=None, event='<Drop>'):
return self.tk.call('dnd', 'bindtarget', window, dndtype, event)
def cleartarget(self, window):
self.tk.call('dnd', 'cleartarget', window)
def bindsource(self, window, callback, dndtype, priority=50):
cmd = self._prepare_tkdnd_func(callback)
self.tk.call('dnd', 'bindsource', window, dndtype, cmd, priority)
def bindsource_query(self, window, dndtype=None):
return self.tk.call('dnd', 'bindsource', window, dndtype)
def clearsource(self, window):
self.tk.call('dnd', 'clearsource', window)
def drag(self, window, actions=None, descriptions=None,
cursorwin=None, callback=None):
cmd = None
if cursorwin is not None:
if callback is not None:
cmd = self._prepare_tkdnd_func(callback)
self.tk.call('dnd', 'drag', window, actions, descriptions,
cursorwin, cmd)
_subst_format = ('%A', '%a', '%b', '%D', '%d', '%m', '%T',
'%W', '%X', '%Y', '%x', '%y')
_subst_format_str = " ".join(_subst_format)
def _prepare_tkdnd_func(self, callback):
funcid = self.master.register(callback, self._dndsubstitute)
cmd = ('%s %s' % (funcid, self._subst_format_str))
return cmd
def _dndsubstitute(self, *args):
if len(args) != len(self._subst_format):
return args
def try_int(x):
x = str(x)
try:
return int(x)
except ValueError:
return x
A, a, b, D, d, m, T, W, X, Y, x, y = args
event = Tkinter.Event()
event.action = A # Current action of the drag and drop operation.
event.action_list = a # Action list supported by the drag source.
event.mouse_button = b # Mouse button pressed during the drag and drop.
event.data = D # The data that has been dropped.
event.descr = d # The list of descriptions.
event.modifier = m # The list of modifier keyboard keys pressed.
event.dndtype = T
event.widget = self.master.nametowidget(W)
event.x_root = X # Mouse pointer x coord, relative to the root win.
event.y_root = Y
event.x = x # Mouse pointer x coord, relative to the widget.
event.y = y
event.action_list = str(event.action_list).split()
for name in ('mouse_button', 'x', 'y', 'x_root', 'y_root'):
setattr(event, name, try_int(getattr(event, name)))
return (event, )
Together with Tkdnd, you will find a tkdnd.tcl program which is a higher level over the own C extension it provides. I didn't wrap this higher level code, but it could be more interesting to replicate it in Python than to use this lower level wrapper.
Things have progressed since this question was originally posted, and tkdnd2.8 (Tcl extensions) and TkinterDnD2 (Python wrapper for tkdnd2.8) are readily available on SourceForge.net.
Here's minimal sample code to do exactly what you've asked.
import Tkinter
from TkinterDnD2 import *
def drop(event):
entry_sv.set(event.data)
root = TkinterDnD.Tk()
entry_sv = Tkinter.StringVar()
entry = Tkinter.Entry(root, textvar=entry_sv, width=80)
entry.pack(fill=Tkinter.X)
entry.drop_target_register(DND_FILES)
entry.dnd_bind('<<Drop>>', drop)
root.mainloop()
You can see How to Install and Use TkDnD with Python 2.7 Tkinter on OSX for download and installation info for both Windows and Mac on Python 2.7 and 3.6.
you can now simple use tkinterdnd2. I've forked it, built it, and upload it to pypi so you could simply pip install tkinterdnd2. usage examples here
or if you are too lasy here's a quick example:
import tkinter as tk
from tkinterdnd2 import DND_FILES, TkinterDnD
root = TkinterDnD.Tk() # notice - use this instead of tk.Tk()
lb = tk.Listbox(root)
lb.insert(1, "drag files to here")
# register the listbox as a drop target
lb.drop_target_register(DND_FILES)
lb.dnd_bind('<<Drop>>', lambda e: lb.insert(tk.END, e.data))
lb.pack()
root.mainloop()
this will open up this
you simply drag over files
(by the way I've used listbox instead of entry but it will work exactly the same)
I recently upgraded to the development release of wxPython (wxPython 2.9.2.4) since I needed the functionality of wx.NotificationMessage within my application. I have been trying unsuccessfully to create notification bubbles on certain user events due to something I think might be a possible bug. Before submitting such bug, I wanted to go ahead and ask the people of the mailing list what they think might be the problem and hopefully find a solution from within my code.
Here is the code I have used:
import wx, sys
app = wx.PySimpleApp()
class TestTaskBarIcon(wx.TaskBarIcon):
def __init__(self):
wx.TaskBarIcon.__init__(self)
# create a test icon
bmp = wx.EmptyBitmap(16, 16)
dc = wx.MemoryDC(bmp)
dc.SetBrush(wx.RED_BRUSH)
dc.Clear()
dc.SelectObject(wx.NullBitmap)
testicon = wx.EmptyIcon()
testicon.CopyFromBitmap(bmp)
self.SetIcon(testicon)
self.Bind(wx.EVT_TASKBAR_LEFT_UP, lambda e: (self.RemoveIcon(),sys.exit()))
wx.NotificationMessage("", "Hello world!").Show()
icon = TestTaskBarIcon()
app.MainLoop()
On my Windows 7 computer, the code creates a small white task bar icon and creates a popup with the phrase "Hello World!". The problem? The message is not on my icon. Another icon is being created and the message is being placed there.
See this image:
http://www.pasteall.org/pic/18068">
What I thought was that this is probably due to the fact that I have passed no parent parameter on line 22:
wx.NotificationMessage("", "Hello world!").Show()
Here is what I changed it to:
wx.NotificationMessage("", "Hello world!", self).Show()
Where 'self' refers to the task bar icon. When I do that, I get an error:
Traceback (most recent call last):
File "C:\Python27\testnotificationmessage.py", line 24, in <module>
icon = TestTaskBarIcon()
File "C:\Python27\testnotificationmessage.py", line 22, in __init__
wx.NotificationMessage("", "Hello world!", self).Show()
File "C:\Python27\lib\site-packages\wx-2.9.2-msw\wx\_misc.py", line 1213, in __init__
_misc_.NotificationMessage_swiginit(self,_misc_.new_NotificationMessage(*args))
TypeError: in method 'new_NotificationMessage', expected argument 3 of type 'wxWindow *'
What's going on? If I remove that argument, I don't get my result, if I add the argument, I get an error! How am I supposed to use wx.NotificationMessage with a wx.TaskBarIcon!
Please help! I hope I've provided enough details. Please comment if you need more!
I would not recommend using 2.9 just yet. I have encountered some strange bugs when trying it out.
You can have the same functionality in 2.8. I am using somewhat modified code that I have found some time ago.
import wx, sys
try:
import win32gui #, win32con
WIN32 = True
except:
WIN32 = False
class BalloonTaskBarIcon(wx.TaskBarIcon):
"""
Base Taskbar Icon Class
"""
def __init__(self):
wx.TaskBarIcon.__init__(self)
self.icon = None
self.tooltip = ""
def ShowBalloon(self, title, text, msec = 0, flags = 0):
"""
Show Balloon tooltip
#param title - Title for balloon tooltip
#param msg - Balloon tooltip text
#param msec - Timeout for balloon tooltip, in milliseconds
#param flags - one of wx.ICON_INFORMATION, wx.ICON_WARNING, wx.ICON_ERROR
"""
if WIN32 and self.IsIconInstalled():
try:
self.__SetBalloonTip(self.icon.GetHandle(), title, text, msec, flags)
except Exception:
pass # print(e) Silent error
def __SetBalloonTip(self, hicon, title, msg, msec, flags):
# translate flags
infoFlags = 0
if flags & wx.ICON_INFORMATION:
infoFlags |= win32gui.NIIF_INFO
elif flags & wx.ICON_WARNING:
infoFlags |= win32gui.NIIF_WARNING
elif flags & wx.ICON_ERROR:
infoFlags |= win32gui.NIIF_ERROR
# Show balloon
lpdata = (self.__GetIconHandle(), # hWnd
99, # ID
win32gui.NIF_MESSAGE|win32gui.NIF_INFO|win32gui.NIF_ICON, # flags: Combination of NIF_* flags
0, # CallbackMessage: Message id to be pass to hWnd when processing messages
hicon, # hIcon: Handle to the icon to be displayed
'', # Tip: Tooltip text
msg, # Info: Balloon tooltip text
msec, # Timeout: Timeout for balloon tooltip, in milliseconds
title, # InfoTitle: Title for balloon tooltip
infoFlags # InfoFlags: Combination of NIIF_* flags
)
win32gui.Shell_NotifyIcon(win32gui.NIM_MODIFY, lpdata)
self.SetIcon(self.icon, self.tooltip) # Hack: because we have no access to the real CallbackMessage value
def __GetIconHandle(self):
"""
Find the icon window.
This is ugly but for now there is no way to find this window directly from wx
"""
if not hasattr(self, "_chwnd"):
try:
for handle in wx.GetTopLevelWindows():
if handle.GetWindowStyle():
continue
handle = handle.GetHandle()
if len(win32gui.GetWindowText(handle)) == 0:
self._chwnd = handle
break
if not hasattr(self, "_chwnd"):
raise Exception
except:
raise Exception, "Icon window not found"
return self._chwnd
def SetIcon(self, icon, tooltip = ""):
self.icon = icon
self.tooltip = tooltip
wx.TaskBarIcon.SetIcon(self, icon, tooltip)
def RemoveIcon(self):
self.icon = None
self.tooltip = ""
wx.TaskBarIcon.RemoveIcon(self)
# ===================================================================
app = wx.PySimpleApp()
class TestTaskBarIcon(BalloonTaskBarIcon):
def __init__(self):
wx.TaskBarIcon.__init__(self)
# create a test icon
bmp = wx.EmptyBitmap(16, 16)
dc = wx.MemoryDC(bmp)
dc.SetBrush(wx.RED_BRUSH)
dc.Clear()
dc.SelectObject(wx.NullBitmap)
testicon = wx.EmptyIcon()
testicon.CopyFromBitmap(bmp)
self.SetIcon(testicon)
self.Bind(wx.EVT_TASKBAR_LEFT_UP, lambda e: (self.RemoveIcon(),sys.exit()))
self.ShowBalloon("", "Hello world!")
icon = TestTaskBarIcon()
app.MainLoop()
There is an undocumented hidden method in TaskBarIcon called ShowBalloon which is only implemented for Windows.
From the source:
def ShowBalloon(*args, **kwargs):
"""
ShowBalloon(self, String title, String text, unsigned int msec=0, int flags=0) -> bool
Show a balloon notification (the icon must have been already
initialized using SetIcon). Only implemented for Windows.
title and text are limited to 63 and 255 characters respectively, msec
is the timeout, in milliseconds, before the balloon disappears (will
be clamped down to the allowed 10-30s range by Windows if it's outside
it) and flags can include wxICON_ERROR/INFO/WARNING to show a
corresponding icon
Returns True if balloon was shown, False on error (incorrect parameters
or function unsupported by OS)
"""
return _windows_.TaskBarIcon_ShowBalloon(*args, **kwargs)
I tested it on Windows with wxPython 2.9.4.0 and it works well.