I'm required to automate an Excel file with Python, the problem is that the Excel has macro in it , when clicking on a macro button it presents a popup:
The popup message box itself has a button that I must click to proceed.
I've tried with both Win32com and Win32 API libraries in Python but I was unable to find any solution in the documentation.
my code is as follows :
ms_excel_app = win32.gencache.EnsureDispatch('Excel.Application')
ms_excel_app.Visible = True
ms_excel_app.DisplayAlerts = False
template_file = r"C:\Templates_for_test\macro_test.xlsm"
ms_excel_file =ms_excel_app.Workbooks.Open(template_file)
ms_excel_file.DisplayAlerts = False
excel_sheet = ms_excel_file.ActiveSheet
# Opening a template file
ms_excel_file = ms_excel_app.Workbooks.Open(template_file)
spread_sheet = ms_excel_file.ActiveSheet
# Click on 'Current date and time' macro button
ms_excel_app.Application.Run("Sheet1.CommandButton1_Click")
# TODO verify verify timestamp and click ok- popup appears verify date and time format : dd/mm/yyyy hh:mm:ss
timestamp_message = time.strftime("%d/%m/%Y %H:%M:%S")
what I'm trying to do is to identify the popup in the screenshot and click on the 'OK' button
A bit late, just post a solution since I faced the same case.
The message box gets Excel application stuck and accordingly blocks your main process. So, a general solution:
start a child thread to listen to the message box -> close it once found
do your main work with the Excel file
stop the child thread when everything is finished
As you're using pywin32, it can also be used to catch and close message box. A Thread class to do the job:
# ButtonClicker.py
import time
from threading import Thread, Event
import win32gui, win32con
class ButtonClicker(Thread):
def __init__(self, title:str, interval:int):
Thread.__init__(self)
self._title = title
self._interval = interval
self._stop_event = Event()
def stop(self):
'''Stop thread.'''
self._stop_event.set()
#property
def stopped(self):
return self._stop_event.is_set()
def run(self):
while not self.stopped:
try:
time.sleep(self._interval)
self._close_msgbox()
except Exception as e:
print(e, flush=True)
def _close_msgbox(self):
# find the top window by title
hwnd = win32gui.FindWindow(None, self._title)
if not hwnd: return
# find child button
h_btn = win32gui.FindWindowEx(hwnd, None,'Button', None)
if not h_btn: return
# show text
text = win32gui.GetWindowText(h_btn)
print(text)
# click button
win32gui.PostMessage(h_btn, win32con.WM_LBUTTONDOWN, None, None)
time.sleep(0.2)
win32gui.PostMessage(h_btn, win32con.WM_LBUTTONUP, None, None)
time.sleep(0.2)
Finally, your code may look like:
from ButtonClicker import ButtonClicker
# 1. start a child thread
# ButtonClicker instance tries to catch window with specified `title` at a user defined frequency.
# In your case, the default title for Excel message box is `Microsoft Excel`.
listener = ButtonClicker("Microsoft Excel", 3)
listener.start()
# 2. do your work with Excel file as before
# though it might be blocked by message box, the concurrent child thread will close it
ms_excel_app = win32.gencache.EnsureDispatch('Excel.Application')
ms_excel_app.Visible = True
ms_excel_app.DisplayAlerts = False
template_file = r"C:\Templates_for_test\macro_test.xlsm"
...
# 3. close the child thread finally
listener.stop()
Related
I wanted to monitor when the text in a tkinter Text widget was modified so that a user could save any new data they had entered. Then on pressing 'Save' I wanted to reset this.
I bound the Text widget's <<Modified>> event to a function so that making any changes to the text would update the 'Save' button from 'disabled' to 'normal' state. After hitting the Save button I ran a function which reset the modified flag and disabled the Save button again until further changes were made.
But I found that it seemed to only fire the event once. Hitting Save didn't reset the button to a 'disabled' state, and editing the text didn't seem to affect the Save button's state either after the first time.
Below is a minimal example to show how the flag doesn't seem to be reset.
EXAMPLE
import tkinter as tk
root = tk.Tk()
def text_modified(event=None):
status_label.config(text="Modified = True")
def reset():
if not text_widget.edit_modified():
return
status_label.config(text="Modified = False")
text_widget.edit_modified(False)
text_widget = tk.Text(root, width=30, height=5)
text_widget.pack()
text_widget.bind("<<Modified>>", text_modified)
status_label = tk.Label(root, text="Modified = False")
status_label.pack()
reset_btn = tk.Button(root, text="Reset", command=reset)
reset_btn.pack()
root.mainloop()
SOLUTION
It turns out that binding the <<Modified>> event to a function means that the function will run not when the Text widget text is changed, but whenever the modified flag is changed - whether it changes to True or to False. So my Save button was saving the data, disabling itself, and resetting the modified flag to False, and this flag change fired the <<Modified>> event, which was bound to a function which un-disabled the Save button again.
Here's a minimal example which shows what's going on. We just need to adjust the function we've bound the <<Modified>> event to so that it deals with modified being False as well:
import tkinter as tk
root = tk.Tk()
def modified_flag_changed(event=None):
if text_widget.edit_modified():
status_label.config(text="Modified = True")
print("Text modified")
else:
print("Modified flag changed to False")
def reset():
if not text_widget.edit_modified():
print("Doesn't need resetting")
return
status_label.config(text="Modified = False")
text_widget.edit_modified(False)
print('Reset')
text_widget = tk.Text(root, width=30, height=5)
text_widget.pack()
text_widget.bind("<<Modified>>", modified_flag_changed)
status_label = tk.Label(root, text="Modified = False")
status_label.pack()
reset_btn = tk.Button(root, text="Reset", command=reset)
reset_btn.pack()
root.mainloop()
I am using Flet module to create a simple GUI.
I want to access the custom data on the button. However, using the clicked method, it seems that the event handler of the button using the data property of it, returns empty string. Therefore, the method does not print anything.
Using flet 0.1.62, Python 3.10
import flet
from flet import UserControl, TextButton, Row, Page
class Test(UserControl):
def __init__(self):
super().__init__()
self.text_button = TextButton(text='test', data='test', on_click=self.clicked)
self.view = Row(controls=[self.text_button])
def clicked(self, e):
print(e.data, ' was clicked')
def build(self):
return self.view
def main(page: Page):
t = Test()
page.add(t)
app = flet.app(target=main)
It seems in the flet's tutorials for the Calculor example, that you can have multiple buttons using the same method for on_click argument.
this is a part from the example's code where it refers to Handling events:
https://flet.dev/docs/tutorials/python-calculator/
Now let's make the calculator do its job. We will be using the same event handler for all the buttons and use data property to differentiate between the actions depending on the button clicked. For each ElevatedButton control, specify on_click=self.button_clicked event and set data property equal to button's text, for example:
ft.ElevatedButton(
text="AC",
bgcolor=ft.colors.BLUE_GREY_100,
color=ft.colors.BLACK,
expand=1,
on_click=self.button_clicked,
data="AC",
)
Below is on_click event handler that will reset the Text value when "AC" button is clicked:
def button_clicked(self, e):
if e.data == "AC":
self.result.value = "0"
With similar approach, specify on_click event and data property for each button and add expected action to the button_clicked event handler depending on e.data value.
I am looking for a way to get the same result using the TextButton,
I'd appreciate your help!
I found the answer to my own question. Apparently in the version of the flet that I am using, the data property has to be looked up through the control instance attribute.
Therefore
I tried this in the code from my question:
def clicked(self, e):
print(e.control.data, ' was clicked')
instead of:
def clicked(self, e):
print(e.data, ' was clicked')
I tried it out of curiosity and it worked.
UPDATE : The problem was solved by removing the window.mainloop() in my second function.
I'm trying to make a game in Python 3.7 using tkinter.
The game begins with a menu (button-widgets in a frame). Clicking in the 'Play' button should open another menu using a different frame. This second menu should contain a 'back' button to return to the first menu.
Each menu is defined in a function. So to go from the main menu to the play menu I call the function playMenu(window) in the function used as command by the 'Play' button.
It looks like this :
def clickButtonPlay():
menuFrame.grid_remove()
playMenu(window)
menuFrame.grid()
In the play menu, the function used as 'back button' command put an end to the function by destroying its frame and using return.
So the program should get back to the clickButtonPlay() function and show the frame of the main menu back, but instead I get a tkinter error :
_tkinter.TclError: can't invoke "grid" command: application has been destroyed
But my frame menuFrame hasn't been destroyed, just un-grid!
Can anyone help me understand what's wrong with the code or find an easier way to do the same thing?
Thank you very much!
Here's a sample of how my program works:
mainMenu file :
import tkinter as tk
from PlayMenu import playMenu
window = tk.Tk()
window.grid()
def menu(window):
def clickButtonPlay():
menuFrame.grid_remove()
playMenu(window)
menuFrame.grid()
menuFrame = tk.Frame(window)
menuFrame.grid()
background = tk.Label(menuFrame, image= backgroundImage)
background.grid()
playButton = tk.Button(menuFrame, image= playButtonImage[0], command= clickButtonPlay)
playButton.place(relx= 0.5, rely= 0.15)
window.mainloop()
menu(window)
playMenu file :
class MyError(Exception):
pass
def _playMenu(window):
def clickButtonBack():
playMenuFrame.destroy()
raise MyError
playMenuFrame = tk.Frame(window)
playMenuFrame.grid()
background = tk.Label(playMenuFrame, image= backgroundImage)
background.grid()
backButton = tk.Button(playMenuFrame, image= backButtonImage[0], command= clickButtonBack)
backButton.place(relx=0.375, rely=0.8)
window.mainloop()
def playMenu(window):
try:
return _playMenu(window)
except MyError:
return
The problem (or at least a problem) is that you're calling mainloop more than once. Each time you call it, a new infinite loop is created. The new loop won't exit until the main window is destroyed. Once that happens, the previous loop will likely throw errors since the widgets it's managing no longer exist.
Need some help please to explain why the following does not work.
Environment: Python 3.4, Gtk3.0, limited experience of Python
File selectcontact.py contains code to select one of a number of records and pass its key back to its parent process for use in one of at least three other actions.
Code snippet from the parent class:
….
self.cindex = 0
….
def editcontact_clicked (self, menuitem):
import selectcontact
selectcontact.SelectContactGUI(self)
print ('From Manage ', self.cindex)
if self.cindex > 0:
import editcontact
editcontact.EditContactGUI(self.db, self.cindex)
….
Code snippet from selectcontact:
class SelectContactGUI:
def init(self, parent_class):
self.builder = Gtk.Builder()
self.builder.add_from_file(UI_FILE)
self.builder.connect_signals(self)
self.parent_class = parent_class
self.db = parent_class.db
self.cursor = self.db.cursor(cursor_factory = psycopg2.extras.NamedTupleCursor)
self.contact_store = self.builder.get_object('contact_store')
self.window = self.builder.get_object('window1')
self.window.show_all()
def select_contact_path(self, path):
self.builder.get_object('treeview_selection1').select_path(path)
def contact_treerow_changed (self, treeview):
selection = self.builder.get_object('treeview_selection1')
model, path = selection.get_selected()
if path != None:
self.parent_class.cindex = model[path][0]
print ('From select ', self.parent_class.cindex)
self.window.destroy()
….
window1 is declared as “modal”, so I was expecting the call to selectcontact to act as a subroutine, so that editcontact wouldn’t be called until control was passed back to the parent. The parent_class bit works because the contact_store is correctly populated. However the transfer back to the parent appears not to work, and the two print statements occur in the wrong order:
From Manage 0
From select 2
Comments gratefully received.
Graeme
"Modal" refers to windows only. That is, a modal window prevents accessing the parent window.
It has little to do with what code is running. I am not familiar with this particular windowing framework, but any I have worked with has had a separate thread for GUI and at least one for processing, to keep the GUI responsive, and message loops running in all active windows, not just the one currently with the focus. The modal dialog has no control over what code in other threads are executed when.
You should be able to break into the debugger and see what threads are running and what is running in each thread at any given time.
I am trying to improve the user experience by showing a load mask above the active QMainWindow/QDialog when performing tasks that takes some time. I have managed to get it working as I want it, except for a moving GIF when performing the task. If I leave the load mask on after the task is complete, the GIF starts moving as it should.
My class for the load mask:
from PyQt4 import QtGui, QtCore
from dlgLoading_view import Ui_dlgLoading
class dlgLoading(QtGui.QDialog, Ui_dlgLoading):
def __init__(self,parent):
QtGui.QDialog.__init__(self,parent)
self.setupUi(self)
self.setWindowFlags(QtCore.Qt.WindowFlags(QtCore.Qt.FramelessWindowHint))
self.setGeometry(0, 0, parent.frameGeometry().width(), parent.frameGeometry().height())
self.setStyleSheet("background-color: rgba(255, 255, 255, 100);")
movie = QtGui.QMovie("loader.gif")
self.lblLoader.setMovie(movie)
movie.start()
def showEvent(self, event):
QtGui.qApp.processEvents()
super(dlgLoading, self).showEvent(event)
def setMessage(self,message):
self.lblMessage.setText(message)
The Ui_dlgLoading contains two labels and some vertical spacers: lblLoader (will contain the gif) and lblMessage (will contain a message if needed)
I create the load mask with this code:
loadmask = dlgLoading(self)
loadmask.setMessage('Reading data... Please wait')
loadmask.show()
I figured I needed some multithreading/multiprocessing, but I can't for the life of me figure out how to do it. I read somewhere that you can't tamper with the GUIs threading, so I would need to move the heavy task there instead, but I'm still blank.
As a simple example, let's say I am trying to load a huge file into memory:
file = open(dataFilename, 'r')
self.dataRaw = file.read()
file.close()
Around that I would create and close my load mask dialog. How do I start the file read without halting the GIF animation?
The GUI is for running some heavy external exe files, so it should work with that too.
I ended up doing this:
class runthread(threading.Thread):
def __init__(self, commandline, cwd):
self.stdout = None
self.stderr = None
self.commandline = commandline
self.cwd = cwd
self.finished = False
threading.Thread.__init__(self)
def run(self):
subprocess.call(self.commandline, cwd=self.cwd)
self.finished = True
class command()
def __init__(self):
...
def run():
...
thread = runthread("\"%s\" \"%s\"" % (os.path.join(self.__caller.exefolder, "%s.exe" % self.__cmdtype), self.__name), self.__caller.exeWorkdir)
thread.start()
count = 0
sleeptime = 0.5
maxcount = 60.0/sleeptime
while True:
time.sleep(sleeptime)
QtWidgets.qApp.processEvents()
count += 1
if thread.finished:
break
if count >= maxcount:
results = QtWidgets.QMessageBox.question(self.__caller, "Continue?", "The process is taking longer than expected. Do you want to continue?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
if results == QtWidgets.QMessageBox.Yes:
count == 0
else:
QtWidgets.QMessageBox.warning(self.__caller, "Process stopped", "The process was stopped")
return False
It actually doesn't directly answer my question, but it worked for me, so I'm posting the answer if others want to do something similar.
I call a process (in this case Pythons subprocess.call) through a thread and track when the process is actually finished. A continuous loop checks periodically if the process is done and updates the GUI (processEvents - this is what triggers the GIF to update). To avoid an infinite loop I offer the user an option to exit after some time.