PyQt 6. Progress bar freezes mainwindow - python-3.x

I am writing program with PyQT6 lib.
In program I have some big calculations (class in 2nd .py file), which take some time. I implemented progress bar and now I am trying to prevent freeze of Gui. I find what I need to use threads and signals to communicate with bar. But it doesn't work, as i can see. Program behavior isn't clear for me because on Linux Ubuntu 18 this program works fine with out freeze.
Can smb help me? What am I doing wrong?
here code example:
mainWindow.py
`
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.resize(800, 150)
self.n_times_clicked = 0
self.setWindowTitle("Выгрузка отчета риска")
self.button = QPushButton("Загрузить файл типа 'Scens'")
self.button.clicked.connect(self.the_button_was_clicked)
self.button.adjustSize()
self.button1 = QPushButton("Загрузить файл типа 'Зоны'")
self.button1.clicked.connect(self.the_button_was_clicked_second)
self.button1.adjustSize()
self.button2 = QPushButton("Переформатировать и выгрузить")
self.button2.clicked.connect(self.btn2_was_clicked)
self.button2.adjustSize()
self.button3 = QPushButton("Переформатировать и выгрузить")
self.button3.clicked.connect(self.btn3_was_clicked)
self.button3.adjustSize()
self.progressbar = QProgressBar()
self.progressbar.setValue(0)
self.input1 = QLineEdit("")
self.input1.setPlaceholderText("Введите путь до файла: C:\\user\\file.docx")
self.input2 = QLineEdit("")
self.input2.setPlaceholderText("Введите путь до файла: C:\\user\\file.docx")
layout = QGridLayout()
layout.addWidget(self.button, 0, 0)
layout.addWidget(self.input1, 0, 1)
layout.addWidget(self.button2, 0, 2)
layout.addWidget(self.button1, 1, 0)
layout.addWidget(self.input2, 1, 1)
layout.addWidget(self.button3, 1, 2)
layout.addWidget(self.progressbar, 2, 0, 2, 3)
self.progressbar.setHidden(True)
layout.setRowStretch(2, 1)
container = QWidget()
container.setLayout(layout)
self.setCentralWidget(container)
def the_button_was_clicked_second(self):
#some code
return fname
def the_button_was_clicked(self):
#some code
return fname
def btn2_was_clicked(self):
self.button2.setEnabled(False)
if get_file_extension(self.input1.text()) == 0:
if Path(self.input1.text()).exists():
self.progressbar.setHidden(False)
saveFile = QFileDialog.getSaveFileName(self, 'Save File', "Новый отчет.docx", "Word файл (*.docx)")
if saveFile[0] != "":
input_data = self.input1.text()
check = 0
check = worker.edit_excel(input_data, saveFile[0])
self.input1.clear()
if check == 1:
msg = QMessageBox()
msg.setWindowTitle("Ошибка")
msg.setText("Структура выбраного файла не соответсвует структуре 'Scens'.")
msg.setIcon(QMessageBox.Icon.Critical)
a = msg.exec()
else:
msg = QMessageBox()
msg.setWindowTitle("Готово!")
msg.setText("Word файл успешно создан")
msg.setIcon(QMessageBox.Icon.Information)
a = msg.exec()
self.progressbar.setValue(0)
self.button2.setEnabled(True)
self.progressbar.setHidden(True)
else:
print("no file selected")
else:
msg = QMessageBox()
msg.setWindowTitle("Ошибка")
msg.setText("Вы выбрали несуществующий файл")
msg.setIcon(QMessageBox.Icon.Critical)
a = msg.exec()
else:
msg = QMessageBox()
msg.setWindowTitle("Ошибка")
msg.setText("Вы выбрали файл неверного расширения. Допустимые файлы *.xls и *.xlsx")
msg.setIcon(QMessageBox.Icon.Critical)
a = msg.exec()
self.input1.clear()
def btn3_was_clicked(self):
#some code
def signal_accept(self, msg):
if int(msg) > 100:
self.progressbar.setValue(100)
else:
self.progressbar.setValue(int(msg))
if self.progressbar.value() == 100:
# self.progressbar.setValue(0)
self.button2.setEnabled(True)
app = QApplication(sys.argv)
window = MainWindow()
worker = Excel_redactor.Excel()
worker._signal.connect(window.signal_accept)
worker.start()
window.show()
sys.exit(app.exec())
`
and in second file I have calculations:
`
class Excel(QThread):
_signal = pyqtSignal(int)
def __init__(self):
super(Excel, self).__init__()
def __del__(self):
self.wait()
def another_e(self, chislo):
power = int(str(chislo)[-2] + str(chislo)[-1])
value_new = float(str(chislo * pow(10, power))[:-2])
return value_new
def sort_dict(self, dict):
dictionary_keys = list(dict.keys())
sorted_dict = {dictionary_keys[x]: sorted(
dict.values())[x] for x in range(len(dictionary_keys))}
return sorted_dict
def edit_excel(self, excel_path, word_path):
#some code
j = 0
step = all_rows / 100
percent = 1
for row in range(2, all_rows-2):
if row % step < 1.0 and row % step >= 0.0:
percent += 1
self._signal.emit(percent)
#some code
return 0
`
I tried to move QThread .start inside btn2_was_clicked func, but in that case gui doesnt work if i do "huge calculations" more than 1 time

Related

Showing a QSplashScreen with QMovie and a QProgressBar crashed. PyQt5

I try to display a SplashScreeen with a gif and a progressbar in it, while a method calculates.
Therefore I have one main.py with a PyQt5 MainWindow Application. In this application method starts, see my calc.py:
from time import sleep, time
import pandas as pd
import concurrent.futures, requests, queue, sys
from threading import Thread
from PyQt5.QtWidgets import *
from PyQt5.QtGui import QIcon, QFont, QKeySequence, QPalette, QBrush, QColor, QPixmap, QMovie, QPainter
from PyQt5.QtCore import Qt, QSize, QRect, QThread, pyqtSignal, QTimer
class MovieSplashScreen(QSplashScreen):
def __init__(self, movie, parent = None):
movie.jumpToFrame(0)
pixmap = QPixmap(movie.frameRect().size())
QSplashScreen.__init__(self, pixmap)
self.movie = movie
self.movie.frameChanged.connect(self.repaint)
def showEvent(self, event):
self.movie.start()
def hideEvent(self, event):
self.movie.stop()
def paintEvent(self, event):
painter = QPainter(self)
pixmap = self.movie.currentPixmap()
self.setMask(pixmap.mask())
painter.drawPixmap(0, 0, pixmap)
def sizeHint(self):
return self.movie.scaledSize()
def splashScreen(zeit = 0):
print('===splashScreen(self)====')
dapp = QApplication(['a', 's'])
# Create and display the splash screen
movie = QMovie("img\\fuchs.gif")
if zeit <= 2:
gerundet = 50
elif zeit > 2:
gerundet = zeit * 60
print("gerundet = ", gerundet)
splash = MovieSplashScreen(movie)
width = splash.frameGeometry().width()
height = splash.frameGeometry().height()
x = splash.pos().x()
y = splash.pos().y()
print('splash x,y: ',width, height, x, y)
splash.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint)
splash.setEnabled(False)
# adding progress bar
palette = QPalette()
palette.setColor(QPalette.Highlight, Qt.green)
progressBar = QProgressBar()
progressBar.setMaximum(gerundet)
progressBar.setGeometry(x, y-30, width, 20)
progressBar.setPalette(palette)
progressBar.setWindowFlags(Qt.SplashScreen | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
gerundet = gerundet + 1
#splash.setMask(splash_pix.mask())
progressBar.show()
splash.show()
splash.showMessage("<h1><font color='red'></font></h1>", Qt.AlignTop | Qt.AlignCenter, Qt.black)
for i in range(1, gerundet):
progressBar.setValue(i)
t = time()
while time() < t + 0.1:
dapp.processEvents()
progressBar.hide()
window = QWidget()
splash.finish(window)
dapp.deleteLater() # here are troubles maybe in cause of main.py with the GUI has a app = QAplllication(sys.argv) too?
def getSignals(selectedCoins, selectedCoinsText):
print("=====getFilteredSignals====")
dfFilter = []
noResults = []
print("selectedCoins: ", selectedCoins)
zeit = len(selectedCoins)
# Problems here?
t = Thread(target=splashScreen, args=(zeit,))
t.start()
for i in range(len(selectedCoins)):
print("i: "+str(i)+" ", selectedCoins[i])
if i >= 1:
sleep(6)
result = makeSignals(selectedCoins[i])
print("results.empty: ", result.empty)
if result.empty == False:
result = result.set_index('Pair', inplace=False)
dfFilter.append(result)
else:
print("selectedCoinsText"+str(i)+": ", selectedCoinsText[i])
noResults.append(selectedCoinsText[i])
print("\nlen(dfFilter): ", len(dfFilter))
if len(dfFilter) == 0:
print("\n\n====in if len(dfFilter) == 0: \n dfFilter: ", dfFilter)
# Creating an empty Dataframe with column names only
dfempty = pd.DataFrame(columns=['User_ID', 'UserName', 'Action'])
print("Empty Dataframe ", dfempty,'\n dfempty.empty: ', dfempty.empty)
return dfempty
elif len(dfFilter) > 0:
for i in range(len(dfFilter)):
print("\n\n====in for loop=== \n dfFilter ["+str(i)+"]: \n", dfFilter[i])
filteredResults = pd.concat(dfFilter, axis=0, sort=False)
#filteredResults['Gain (%)'] = pd.to_numeric(filteredResults['Gain (%)'], errors='coerce')
filteredResults = filteredResults.sort_values(by='Gain (%)', ascending=False, inplace=False)
filteredResults = filteredResults.reset_index(inplace=False)
print('\nfilteredResults: \n', filteredResults, "\n", filteredResults.dtypes)
return filteredResults
self.results = calc.getSignals( a, aText)
Splashscreen and progressebar are displayed, but then the gui freezes and crashed.
So from main.py calc.py is started.
main.py is a gui with app = Qapplication() and a MainWindow().
Looks like:
import calc
class MainWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.font = QFont("Helvetica", 9)
self.setFont(self.font)
...
self.getSignals(a, aText)
...
def getSignals(self, a, aText):
zeit = len(a)
self.results = calc.getSignals(a, aText)
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setStyle('Fusion')
mw = MainWindow()
mw.show()
sys.exit(app.exec_())
I try to use dapp.exit(exec_()) instead of dapp.deleteLater() in calc.py , but it still crashed too.
You are confusing concepts:
The time consuming task must be executed in another thread, in your case that task is processing.
If you want to control the GUI with the information obtained in the other thread then you must use signals, in this case I have created 2 signals that send the value of the QProgressBar showing the windows, and the other one sends the result.
If you want to do periodic tasks then use a QTimer.
With the above the solution is:
import os
import threading
import time
from PyQt5 import QtCore, QtGui, QtWidgets
import pandas as pd
CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))
class MovieSplashScreen(QtWidgets.QSplashScreen):
def __init__(self, movie, parent=None):
self._movie = movie
self.movie.jumpToFrame(0)
pixmap = QtGui.QPixmap(movie.frameRect().size())
super(MovieSplashScreen, self).__init__(pixmap)
self.setParent(parent)
self.movie.frameChanged.connect(self.repaint)
#property
def movie(self):
return self._movie
def showEvent(self, event):
self.movie.start()
super(MovieSplashScreen, self).showEvent(event)
def hideEvent(self, event):
self.movie.stop()
super(MovieSplashScreen, self).hideEvent(event)
def paintEvent(self, event):
painter = QtGui.QPainter(self)
pixmap = self.movie.currentPixmap()
self.setMask(pixmap.mask())
painter.drawPixmap(0, 0, pixmap)
def sizeHint(self):
return self.movie.scaledSize()
class Processor(QtCore.QObject):
started = QtCore.pyqtSignal(int)
filteredResultsSignal = QtCore.pyqtSignal(pd.DataFrame)
def execute(self, selectedCoins, selectedCoinsText):
threading.Thread(
target=self._execute, args=(selectedCoins, selectedCoinsText)
).start()
def _execute(self, selectedCoins, selectedCoinsText):
print("=====getFilteredSignals====")
dfFilter = []
noResults = []
print("selectedCoins: ", selectedCoins)
zeit = len(selectedCoins)
self.started.emit(zeit)
for i, (coin, text) in enumerate(zip(selectedCoins, selectedCoinsText)):
print("i: {} {}".format(i, coin))
if i >= 6:
time.sleep(6)
result = makeSignals(selectedCoins[i])
print("results.empty: ", result.empty)
if not result.empty:
result = result.set_index("Pair", inplace=False)
dfFilter.append(result)
else:
print("selectedCoinsText{}: {}".format(i, text))
noResults.append(text)
print("\nlen(dfFilter): ", len(dfFilter))
if len(dfFilter) == 0:
print("\n\n====in if len(dfFilter) == 0: \n dfFilter: ", dfFilter)
# Creating an empty Dataframe with column names only
filteredResults = pd.DataFrame(columns=["User_ID", "UserName", "Action"])
print(
"Empty Dataframe ",
filteredResults,
"\n dfempty.empty: ",
filteredResults.empty,
)
elif len(dfFilter) > 0:
for i, df_filter in enumerate(dfFilter):
print(
"\n\n====in for loop=== \n dfFilter [{}]: \n{}".format(i, df_filter)
)
filteredResults = pd.concat(dfFilter, axis=0, sort=False)
# filteredResults['Gain (%)'] = pd.to_numeric(filteredResults['Gain (%)'], errors='coerce')
filteredResults = filteredResults.sort_values(
by="Gain (%)", ascending=False, inplace=False
)
filteredResults = filteredResults.reset_index(inplace=False)
print(
"\nfilteredResults: \n", filteredResults, "\n", filteredResults.dtypes
)
self.filteredResultsSignal.emit(filteredResults)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
font = QtGui.QFont("Helvetica", 9)
self.setFont(font)
self.processor = Processor(self)
self.processor.started.connect(self.on_started)
self.processor.filteredResultsSignal.connect(self.on_filteredResultsSignal)
self.processor.execute("a", "aText")
movie_path = os.path.join(CURRENT_DIR, "img", "fuchs.gif")
movie = QtGui.QMovie(movie_path)
self.splash = MovieSplashScreen(movie)
self.splash.setWindowFlags(
QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.FramelessWindowHint
)
self.splash.setEnabled(False)
# adding progress bar
palette = QtGui.QPalette()
palette.setColor(QtGui.QPalette.Highlight, QtCore.Qt.green)
self.progressBar = QtWidgets.QProgressBar()
self.progressBar.setPalette(palette)
self.progressBar.setWindowFlags(
QtCore.Qt.SplashScreen
| QtCore.Qt.FramelessWindowHint
| QtCore.Qt.WindowStaysOnTopHint
)
self.progressBar.move(self.splash.pos() + QtCore.QPoint(0, -30))
self.progressBar.resize(self.splash.width(), 30)
self.counter = 0
self.gerundet = 0
self.timer = QtCore.QTimer(interval=100, timeout=self.on_timeout)
#QtCore.pyqtSlot(pd.DataFrame)
def on_filteredResultsSignal(self, df):
print(df)
#QtCore.pyqtSlot()
#QtCore.pyqtSlot(int)
def on_started(self, zeit=0):
gerundet = 50 if zeit <= 2 else zeit + 60
print("gerundet = ", gerundet)
gerundet = gerundet + 1
self.progressBar.setMaximum(gerundet)
# splash.setMask(splash_pix.mask())
self.progressBar.show()
self.splash.show()
self.splash.showMessage(
"<h1><font color='red'></font></h1>",
QtCore.Qt.AlignTop | QtCore.Qt.AlignCenter,
QtCore.Qt.black,
)
self.counter = 1
self.gerundet = gerundet
self.timer.start()
#QtCore.pyqtSlot()
def on_timeout(self):
self.progressBar.setValue(self.counter)
self.counter += 1
if self.counter > self.gerundet:
self.timer.stop()
self.progressBar.hide()
self.splash.close()
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
app.setStyle("Fusion")
mw = MainWindow()
mw.show()
sys.exit(app.exec_())

Getting spinner working without threading

I am trying to get a spinner working, but without using threads (I had a working version, but it gave me erratic behaviour. After reading online people told me that it's best to avoid threading with tkinter). So I wanted to give the after function a try.
The goal is to have a spinner pop up before starting something is done and have it disappear once the 'something' is done. Here is a minimalistic example of what I have:
import tk
from time import sleep
class Spinner(tk.Toplevel):
def __init__(self, master, text = None, barsize = 10, speed = 100, spinnerSize = 50):
super().__init__(master = master)
self.barsize = barsize
self.speed = speed
self.size = spinnerSize
self.done = False
self.progress = 0
self.forward = True
if text is None:
self.text = 'Something is happening'
else:
self.text = text
self.title(self.text)
self.out = tk.Label(master = self,
text = self.spinnerCalc(),
height = 1,
borderwidth = 0,
width = 80,
bg = self['background'])
self.out.pack()
self.update_self()
def update_self(self):
print(self.progress)
if self.forward:
if self.progress == self.size - self.barsize:
self.forward = False
self.progress -= 1
else:
self.progress += 1
else:
if self.progress == 0:
self.forward = True
self.progress += 1
else:
self.progress -= 1
self.out.config(text = self.spinnerCalc())
if self.done:
self.grab_release()
self.destroy()
else:
# print('entered continue update')
self.update()
# print('step 2')
self.grab_set()
# print('step 3')
self.after(self.speed, self.update_self)
# print('step 4')
def spinnerCalc(self):
#returns something like |------█████--------------|
bar = '|'
barsize = self.barsize
size = self.size
for i in range (self.progress):
bar += '-'
for i in range (barsize):
bar += '\u2588'
for i in range (size-barsize-self.progress):
bar += '-'
bar += '|'
return bar
def stop(self):
self.done = True
root = tk.Tk()
def spin():
spinner = Spinner(root)
a_bunch_of_stuff_happening(10)
spinner.stop()
def a_bunch_of_stuff_happening(amount):
count = 0
for i in range(100000000):
count += i
print(count)
tk.Button(master = root,
text = 'Press me',
command = spin).pack()
tk.Button(master = root,
text = 'Close me',
command = root.destroy).pack()
root.mainloop()
The problem is that the update_self is not running. I do not want to mainloop the Spinner as that will block my a_bunch_of_stuff_happening() function. The a_bunch_of_stuff_happening is also a complicated function which changes attributes of the main root class.
I am stuck with what else to try without going into threading again (which was a big pain last time). Anyone have advice what else I could try to get this to work?

How to safely quit a Toplevel() window on a thread

I want to include a spinner in my program to show some indication that program is doing something when some heavy background calculations are being made. Here is the spinner I came up with, but I am having trouble quitting it safely. I am not sure how else I can make my idea work, and this is my latest code for it. Anyone have any hint on how I can implement this functionality?
import tkinter as tk
from threading import Thread
from time import sleep
class Spinner_top(tk.Toplevel):
def __init__(self, master, text = None, barsize = 10, speed = 0.10, spinnerSize = 50):
super().__init__(master = master)
self.barsize = barsize
self.speed = speed
self.size = spinnerSize
self.done = False
if text is None:
self.text = 'Program is thinking, thus progress is moving'
else:
self.text = text
self.title(self.text)
self.out = tk.Label(master = self,
height = 1,
borderwidth = 0,
width = 80,
bg = self['background'])
self.out.pack()
self.update()
self.thread = Thread(target = self.fill_self)
self.thread.start()
def fill_self(self):
print('start called')
# print(self)
# print('update2')
forward = True
progress = 0
print('entered while loop')
while True:
msg = self.spinnerCalc(progress)
print('message changed')
self.out.configure(text = msg)
print('message updated')
self.update()
print('updated self')
if forward:
if progress == self.size - self.barsize:
forward = False
progress -= 1
else:
progress += 1
else:
if progress == 0:
forward = True
progress += 1
else:
progress -= 1
print(self.done)
if self.done:
break
sleep(self.speed)
return
def spinnerCalc(self, progress):
bar = '|'
barsize = self.barsize
size = self.size
for i in range (progress):
bar += '-'
for i in range (barsize):
bar += '\u2588'
for i in range (size-barsize-progress):
bar += '-'
bar += '|'
return bar
def stop(self):
print('stop called')
self.done = True
self.thread.join()
print('got pass join()')
self.quit()
self.destroy()
root = tk.Tk()
spinner = [None]
def start():
if spinner[0] is None:
spinner[0] = Spinner_top(root,'Program is thinking')
def stop():
if spinner[0] is not None:
spinner[0].stop()
spinner[0] = None
def experiment():
if spinner[0] is None:
spinner[0] = Spinner_top(root,'Try something')
spinner[0].stop()
tk.Button(root,
text = 'start spinner',
command = start).pack()
tk.Button(root,
text = 'stop spinner',
command = stop).pack()
tk.Button(root,
text='experiment',
command = experiment).pack()
tk.Button(root,
text = 'quit',
command = root.destroy).pack()
root.mainloop()
I am running into 2 problems with this:
1. When starting using the 'start spinner' button, the program freezes up.
2. When starting with the 'experiment' button, the code cannot get to print('message updated') line.
#trying to use the spinner using after as per #Nae suggestion (resulted in more problems :/
class Spinner_top(tk.Toplevel):
def __init__(self, master, text = None, barsize = 10, speed = 100, spinnerSize = 50):
super().__init__(master = master)
self.barsize = barsize
self.speed = speed
self.size = spinnerSize
self.progress = 0
self.done = False
if text is None:
self.text = 'Program is thinking, thus progress is moving'
else:
self.text = text
self.title(self.text)
self.out = tk.Label(master = self,
height = 1,
borderwidth = 0,
width = 80,
bg = self['background'])
self.out.pack()
self.fill_self()
def fill_self(self):
print('start called')
# print(self)
# print('update2')
self.forward = True
progress = 0
print('entered while loop')
def foo():
msg = self.spinnerCalc(progress)
print('message changed')
self.out.configure(text = msg)
print('message updated')
self.update()
print('updated self')
if self.forward:
if self.progress == self.size - self.barsize:
self.forward = False
self.progress -= 1
else:
self.progress += 1
else:
if progress == 0:
self.forward = True
self.progress += 1
else:
self.progress -= 1
# print(self.done)
if not self.done:
self.after(self.speed, func = foo)
foo()
return
def spinnerCalc(self, progress):
bar = '|'
barsize = self.barsize
size = self.size
for i in range (progress):
bar += '-'
for i in range (barsize):
bar += '\u2588'
for i in range (size-barsize-progress):
bar += '-'
bar += '|'
return bar
def stop(self):
print('stop called')
self.done = True
self.quit()
self.destroy()
After trying to figure this out, I have just resorted to simple spinner. Better than nothing and technically does the job. If anyone finds a way to make this spinner idea work with an updating gui though, I'd appreciate it.
class Simple_Spinner(tk.Toplevel):
def __init__(self,master,text=None):
super().__init__(master)
self.grab_set()
self.protocol("WM_DELETE_WINDOW", self.do_nothing)
if text is None:
text = 'Program is processing'
tk.Label(self,text=text).pack()
self.update()
def do_nothing(self):
return
def stop(self):
self.grab_release()
self.destroy()
If you can't measure the progress and just want to show it's busy, and user is not supposed to do other things meanwhile, just change the window mouse cursor to a watch:
>>> import tkinter
>>> root = tkinter.Tk()
>>> root.configure(cursor='watch')
and revert in the end with
>>> root.configure(cursor='arrow')
you can adapt this to your code and is about as simple as I can think.

Display of a large amount of data in a Canvas with a Scrollbar

I'm having a problem trying to display a large table in tkinter.
First, I tried to display all label at once in the canvas, but over few hundred rows, the program shut down. So I tried to create a scrollable canvas that updates everytime I scroll: I collect the position of the scrollbar and depending on the position of it, I display the 10 values corresponding.
But I can't get this code working. For now it only displays a black background with the scrollbar on the right.
Here is the code:
from tkinter import *
class Application(object):
def __init__(self, parent):
self.x = []
for i in range(1, 1000):
self.x.append(i)
self.parent = parent
self.mainFrame = Frame(self.parent)
self.mainFrame.pack()
self.canvas = Canvas(self.mainFrame, width = 200, height = 500, bg = "black")
self.canvas.grid(row = 0, column = 0)
self.scroll = Scrollbar(self.mainFrame, orient = VERTICAL, command = self.update)
self.scroll.grid(row = 0, column = 1)
self.canvas.configure(yscrollcommand = self.scroll.set)
self.tabCursor = 0
self.scrollPosition = self.scroll.get()
def update(self):
self.tabCursor = round(self.scrollPosition[0]*len(self.x))
if ((len(self.x) - self.tabCursor) < 10):
self.tabCursor = len(self.x) - 10
for i in range(0, 10): #display 10 values
label = Label(self.canvas, text = str(self.x[tabCursor + i]), width = 50)
label.grid(column = 0, row = i)
if __name__ == '__main__':
root = Tk()
app = Application(root)
root.mainloop()
EDIT :
I finally had time to implement your answer. It looks fine but I can't get the scrollbar working and i don't know why.
class TableauDeDonnees(object):
"Tableau de données -- Onglet Tableau de données"
def __init__(self, data, parent):
self.parent = parent
self.data = data
print(self.data[0], self.data[1])
print(len(self.data[0]), len(self.data[1]))
self.labels = []
self.navigationFrame = Frame(self.parent)
self.canvas = Canvas(self.parent, bg = "black", width = 200, height = 500)
self.mainFrame = Frame(self.canvas)
self.navigationFrame.pack()
print(len(data))
for row in range(50):
for column in range(len(data)):
self.labels.append(Label(self.canvas, text = str(data[column][row])))
for i in range(len(self.labels)):
self.labels[i].grid(row = i // 2, column = i % 2, sticky = NSEW)
self.boutonRetour = Button(self.navigationFrame, text = "Retour", command = lambda: self.move(-2))
self.quickNav = Entry(self.navigationFrame, width = 3)
self.quickNav.bind('<Return>', lambda x: self.move(self.quickNav.get()))
self.boutonSuivant = Button(self.navigationFrame, text = "Suivant", command = lambda: self.move(0))
temp = divmod(len(data[0]), len(self.labels) // 2)
self.pages = temp[0] + (1 if temp[1] else 0)
self.position = Label(self.navigationFrame, text='Page 1 sur ' + str(self.pages))
self.pageCourante = 1
self.boutonRetour.grid(row = 0, column = 0)
self.quickNav.grid(row = 0, column = 1)
self.boutonSuivant.grid(row = 0, column = 2)
self.position.grid(row = 0, column = 3)
self.scroll = Scrollbar(self.parent, orient = VERTICAL, command = self.canvas.yview)
self.canvas.configure(yscrollcommand = self.scroll.set)
self.scroll.pack(side = RIGHT, fill='y')
self.canvas.pack(side = LEFT, fill = 'both')
self.canvas.create_window((4,4), window = self.mainFrame, anchor = "nw", tags = "frame")
self.canvas.configure(yscrollcommand = self.scroll.set)
self.mainFrame.bind("<Configure>", self.update)
self.canvas.configure(scrollregion = self.canvas.bbox("all"))
def update(self, event):
self.canvas.configure(scrollregion = self.canvas.bbox("all"))
def move(self, direction):
if (self.pageCourante == 1 and direction == -2) or (self.pageCourante == self.pages and direction == 0):
return
if direction in (-2, 0):
self.pageCourante += direction + 1
else:
try:
temp = int(direction)
if temp not in range(1, self.pages + 1):
return
except ValueError:
return
else:
self.pageCourante = temp
for i in range(len(self.labels)):
try:
location = str(self.data[i % 2][len(self.labels)*(self.pageCourante - 1) + i])
except IndexError:
location = ''
self.labels[i].config(text = location)
self.position.config(text = 'Page ' + str(self.pageCourante) + ' sur ' + str(self.pages))
I don't understand why the scrollbar isn't working properly. Note, that my parent is a notebook.
Also, there is a problem with the number of items displayed. The number of pages is right but it seems it displays more than it should cause last pages are empty and the last values displayed seems right.
Thank you for your attention
The scrollbar doesn't work by continuously creating new widgets ad infinitum. You were also missing some key parts - unfortunately, Scrollbar isn't as straightforward as most tkinter widgets.
from tkinter import *
class Application(object):
def __init__(self, parent):
self.parent = parent
self.canvas = Canvas(self.parent, bg='black', width = 200, height = 500)
self.mainFrame = Frame(self.canvas)
self.scroll = Scrollbar(self.parent, orient = VERTICAL, command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.scroll.set)
self.scroll.pack(side='right', fill='y')
self.canvas.pack(side='left', fill='both')
self.canvas.create_window((4,4), window=self.mainFrame, anchor="nw", tags="frame")
self.canvas.configure(yscrollcommand = self.scroll.set)
self.mainFrame.bind("<Configure>", self.update)
self.x = []
for i in range(1000):
self.x.append(Label(self.mainFrame, text=str(i)))
self.x[i].grid()
def update(self, event):
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
if __name__ == '__main__':
root = Tk()
app = Application(root)
root.mainloop()
If you'd like to show only a few at a time and provide a forum-like interface, you can use Buttons to navigate between pages. This example allows the user to navigate with Back and Forward buttons, as well as by entering a page number in the box and pressing Enter.
from tkinter import *
class Application(object):
def __init__(self, parent):
self.x = list(range(1000))
self.labels = []
self.parent = parent
self.navigation_frame = Frame(self.parent)
self.canvas = Canvas(self.parent, bg='black', width = 200, height = 500)
self.mainFrame = Frame(self.canvas)
self.navigation_frame.pack()
for i in range(100):
self.labels.append(Label(self.mainFrame, text=str(i)))
self.labels[i].grid()
self.back_button = Button(self.navigation_frame, text='Back', command=lambda: self.move(-2))
self.quick_nav = Entry(self.navigation_frame, width=3)
self.quick_nav.bind('<Return>', lambda x: self.move(self.quick_nav.get()))
self.forward_button = Button(self.navigation_frame, text='Forward', command=lambda: self.move(0))
temp = divmod(len(self.x), len(self.labels))
self.pages = temp[0] + (1 if temp[1] else 0)
self.you_are_here = Label(self.navigation_frame, text='Page 1 of ' + str(self.pages))
self.current_page = 1
self.back_button.grid(row=0, column=0)
self.quick_nav.grid(row=0, column=1)
self.forward_button.grid(row=0, column=2)
self.you_are_here.grid(row=0, column=3)
self.scroll = Scrollbar(self.parent, orient = VERTICAL, command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.scroll.set)
self.scroll.pack(side='right', fill='y')
self.canvas.pack(side='left', fill='both')
self.canvas.create_window((4,4), window=self.mainFrame, anchor="nw", tags="frame")
self.canvas.configure(yscrollcommand = self.scroll.set)
self.mainFrame.bind("<Configure>", self.update)
def update(self, event):
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
def move(self, direction):
if (self.current_page == 1 and direction == -2) or (self.current_page == self.pages and direction == 0):
return
if direction in (-2, 0):
self.current_page += direction + 1
else:
try:
temp = int(direction)
if temp not in range(1, self.pages+1):
return
except ValueError:
return
else:
self.current_page = temp
for i in range(len(self.labels)):
try:
location = str(self.x[len(self.labels)*(self.current_page - 1) + i])
except IndexError:
location = ''
self.labels[i].config(text=location)
self.you_are_here.config(text='Page ' + str(self.current_page) + ' of ' + str(self.pages))
if __name__ == '__main__':
root = Tk()
app = Application(root)
root.mainloop()

wxPython: Problems with thread

My program keeps expanding.
In my MainProgram method in the MainPanel class I'm using some functions from an other program. While using this the GUI hangs until that is finished and I want to solve this by using a new thread for this method.
While doing this I get an error when executing OnRun. It says:
Unhandled exception in thread started by <bound method MainPanel.OnIndex of <__main__.MainPanel; proxy of <Swig Object of type 'wxPanel *' at 0x526e238> >>
It think this has something to do with the OnIndex setting som values to self.textOutput. Now, how can I solve this little problem of mine?
Help is much appreciated! =)
import wx, thread
ID_EXIT = 110
class MainPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.buttonRun = wx.Button(self, label="Run")
self.buttonRun.Bind(wx.EVT_BUTTON, self.OnRun )
self.buttonExit = wx.Button(self, label="Exit")
self.buttonExit.Bind(wx.EVT_BUTTON, self.OnExit)
self.labelChooseRoot = wx.StaticText(self, label ="Root catalog: ")
self.labelScratchWrk = wx.StaticText(self, label ="Scratch workspace: ")
self.labelMergeFile = wx.StaticText(self, label ="Merge file: ")
self.textChooseRoot = wx.TextCtrl(self, size=(210, -1))
self.textChooseRoot.Bind(wx.EVT_LEFT_UP, self.OnChooseRoot)
self.textScratchWrk = wx.TextCtrl(self, size=(210, -1))
self.textMergeFile = wx.TextCtrl(self, size=(210, -1))
self.textOutput = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_READONLY)
self.sizerF = wx.FlexGridSizer(3, 2, 5, 5)
self.sizerF.Add(self.labelChooseRoot) #row 1, col 1
self.sizerF.Add(self.textChooseRoot) #row 1, col 2
self.sizerF.Add(self.labelScratchWrk) #row 2, col 1
self.sizerF.Add(self.textScratchWrk) #row 2, col 2
self.sizerF.Add(self.labelMergeFile) #row 3, col 1
self.sizerF.Add(self.textMergeFile) #row 3, col 2
self.sizerB = wx.BoxSizer(wx.VERTICAL)
self.sizerB.Add(self.buttonRun, 1, wx.ALIGN_RIGHT|wx.ALL, 5)
self.sizerB.Add(self.buttonExit, 0, wx.ALIGN_RIGHT|wx.ALL, 5)
self.sizer1 = wx.BoxSizer()
self.sizer1.Add(self.sizerF, 0, wx.ALIGN_RIGHT | wx.EXPAND | wx.ALL, 10)
self.sizer1.Add(self.sizerB, 0, wx.ALIGN_RIGHT | wx.EXPAND | wx.ALL)
self.sizer2 = wx.BoxSizer()
self.sizer2.Add(self.textOutput, 1, wx.EXPAND | wx.ALL, 5)
self.sizerFinal = wx.BoxSizer(wx.VERTICAL)
self.sizerFinal.Add(self.sizer1, 0, wx.ALIGN_RIGHT | wx.EXPAND | wx.ALL)
self.sizerFinal.Add(self.sizer2, 1, wx.ALIGN_RIGHT | wx.EXPAND | wx.ALL)
self.SetSizerAndFit(self.sizerFinal)
def OnChooseRoot(self, event):
dlg = wx.DirDialog(self, "Choose a directory:", style=wx.DD_DEFAULT_STYLE)
if dlg.ShowModal() == wx.ID_OK:
root_path = dlg.GetPath()
self.textChooseRoot.SetValue(root_path)
dlg.Destroy()
def OnRun(self, event):
#Check first if input values are
thread.start_new_thread(self.OnIndex, ())
def OnIndex(self):
#Do something and post to self.textOutput what you do.
def OnExit(self, event):
self.GetParent().Close()
class MainWindow(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, title="IndexGenerator", size=(430, 330),
style=((wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE |
wx.STAY_ON_TOP) ^ wx.RESIZE_BORDER))
self.CreateStatusBar()
self.fileMenu = wx.Menu()
self.fileMenu.Append(ID_EXIT, "E&xit", "Exit the program")
self.menuBar = wx.MenuBar()
self.menuBar.Append(self.fileMenu, "&File")
self.SetMenuBar(self.menuBar)
wx.EVT_MENU(self, ID_EXIT, self.OnExit)
self.Panel = MainPanel(self)
self.CentreOnScreen()
self.Show()
def OnExit(self, event):
self.Close()
if __name__ == "__main__":
app = wx.App(False)
frame = MainWindow()
app.MainLoop()
[EDIT:] Here is an extract of the OnRun and OnIndex methods. Is there a () to much or maybe an , ?
def OnRun(self, Event=None):
#---Check input values, continue if not wrong
if self.CheckValid() == 0:
#Get root directory
root_path = self.textChooseRoot.GetValue()
#Get scratch workspace
scratch_workspace =self.textScratchWrk.GetValue()
#Get merge file
merge_fil = self.textMergeFile.GetValue()
thread.start_new_thread(self.OnIndex, (root_path,scratch_workspace,merge_fil))
def showmsg(self, msg):
self.textOutput.AppendText(msg + "\n")
def OnIndex(self,root_path,scratch_workspace,merge_fil):
#---PUBSUB - publishes a message to the "show.statusbar"
## msg = "Please wait...Program is running..."
## Publisher().sendMessage(("show.statusbar"), msg)
#---START INDEX GENERATOR CODE
gp.overwriteoutput = 1
gp.OutputMFlag = "DISABLED"
gp.OutputZFlag = "DISABLED"
fc_List = {}
#Get log file. For now a constant. Needs to be changed.
logfil = open("C:\\Python26\\Code\\log.txt", mode = "w")
fold_nr = 0
for root_fold, dirs, files in os.walk(root_path):
root_fold_low = root_fold.lower()
if not root_fold_low.find(".gdb") > -1:
fold_nr += 1
tot_t = time.clock()
wx.CallAfter(self.textOutput.AppendText, ("Mappe : " + str(fold_nr) + " : " + root_fold + "\n"))
All interaction with wx object should be in the main thread.
An easy fix would be to use something like wx.CallAfter(self.textOutput.SetValue, "output") instead of self.textOutput.SetValue("output").
wx.CallAfter sends to the main even loop what to execute as soon as it gets around to it and since the main loop is in the main thread everything works out fine.
UPDATE: Working merged code snippets:
import wx, thread, os, time
ID_EXIT = 110
class Dummy:
pass
gp = Dummy()
class MainPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.buttonRun = wx.Button(self, label="Run")
self.buttonRun.Bind(wx.EVT_BUTTON, self.OnRun )
self.buttonExit = wx.Button(self, label="Exit")
self.buttonExit.Bind(wx.EVT_BUTTON, self.OnExit)
self.labelChooseRoot = wx.StaticText(self, label ="Root catalog: ")
self.labelScratchWrk = wx.StaticText(self, label ="Scratch workspace: ")
self.labelMergeFile = wx.StaticText(self, label ="Merge file: ")
self.textChooseRoot = wx.TextCtrl(self, size=(210, -1))
self.textChooseRoot.Bind(wx.EVT_LEFT_UP, self.OnChooseRoot)
self.textScratchWrk = wx.TextCtrl(self, size=(210, -1))
self.textMergeFile = wx.TextCtrl(self, size=(210, -1))
self.textOutput = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_READONLY)
self.sizerF = wx.FlexGridSizer(3, 2, 5, 5)
self.sizerF.Add(self.labelChooseRoot) #row 1, col 1
self.sizerF.Add(self.textChooseRoot) #row 1, col 2
self.sizerF.Add(self.labelScratchWrk) #row 2, col 1
self.sizerF.Add(self.textScratchWrk) #row 2, col 2
self.sizerF.Add(self.labelMergeFile) #row 3, col 1
self.sizerF.Add(self.textMergeFile) #row 3, col 2
self.sizerB = wx.BoxSizer(wx.VERTICAL)
self.sizerB.Add(self.buttonRun, 1, wx.ALIGN_RIGHT|wx.ALL, 5)
self.sizerB.Add(self.buttonExit, 0, wx.ALIGN_RIGHT|wx.ALL, 5)
self.sizer1 = wx.BoxSizer()
self.sizer1.Add(self.sizerF, 0, wx.ALIGN_RIGHT | wx.EXPAND | wx.ALL, 10)
self.sizer1.Add(self.sizerB, 0, wx.ALIGN_RIGHT | wx.EXPAND | wx.ALL)
self.sizer2 = wx.BoxSizer()
self.sizer2.Add(self.textOutput, 1, wx.EXPAND | wx.ALL, 5)
self.sizerFinal = wx.BoxSizer(wx.VERTICAL)
self.sizerFinal.Add(self.sizer1, 0, wx.ALIGN_RIGHT | wx.EXPAND | wx.ALL)
self.sizerFinal.Add(self.sizer2, 1, wx.ALIGN_RIGHT | wx.EXPAND | wx.ALL)
self.SetSizerAndFit(self.sizerFinal)
def OnChooseRoot(self, event):
dlg = wx.DirDialog(self, "Choose a directory:", style=wx.DD_DEFAULT_STYLE)
if dlg.ShowModal() == wx.ID_OK:
root_path = dlg.GetPath()
self.textChooseRoot.SetValue(root_path)
dlg.Destroy()
def CheckValid(self):
return 0
def OnRun(self, Event=None):
#---Check input values, continue if not wrong
if self.CheckValid() == 0:
#Get root directory
root_path = self.textChooseRoot.GetValue()
#Get scratch workspace
scratch_workspace =self.textScratchWrk.GetValue()
#Get merge file
merge_fil = self.textMergeFile.GetValue()
thread.start_new_thread(self.OnIndex, (root_path,scratch_workspace,merge_fil))
def showmsg(self, msg):
self.textOutput.AppendText(msg + "\n")
def OnIndex(self,root_path,scratch_workspace,merge_fil):
#---PUBSUB - publishes a message to the "show.statusbar"
## msg = "Please wait...Program is running..."
## Publisher().sendMessage(("show.statusbar"), msg)
#---START INDEX GENERATOR CODE
gp.overwriteoutput = 1
gp.OutputMFlag = "DISABLED"
gp.OutputZFlag = "DISABLED"
fc_List = {}
#Get log file. For now a constant. Needs to be changed.
#logfil = open("C:\\Python26\\Code\\log.txt", mode = "w")
fold_nr = 0
for root_fold, dirs, files in os.walk(root_path):
root_fold_low = root_fold.lower()
if not root_fold_low.find(".gdb") > -1:
fold_nr += 1
tot_t = time.clock()
wx.CallAfter(self.textOutput.AppendText, ("Mappe : " + str(fold_nr) + " : " + root_fold + "\n"))
def OnExit(self, event):
self.GetParent().Close()
class MainWindow(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, title="IndexGenerator", size=(430, 330),
style=((wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE |
wx.STAY_ON_TOP) ^ wx.RESIZE_BORDER))
self.CreateStatusBar()
self.fileMenu = wx.Menu()
self.fileMenu.Append(ID_EXIT, "E&xit", "Exit the program")
self.menuBar = wx.MenuBar()
self.menuBar.Append(self.fileMenu, "&File")
self.SetMenuBar(self.menuBar)
wx.EVT_MENU(self, ID_EXIT, self.OnExit)
self.Panel = MainPanel(self)
self.CentreOnScreen()
self.Show()
def OnExit(self, event):
self.Close()
if __name__ == "__main__":
app = wx.App(False)
frame = MainWindow()
app.MainLoop()
You just need to use a threadsafe method to send information back to the main thread. As the others have stated, you cannot interact with the main thread directly or weird things will happen. Here's a really good article on the various ways you can accomplish this feat:
http://wiki.wxpython.org/LongRunningTasks
And here's another article on the subject:
http://www.blog.pythonlibrary.org/2010/05/22/wxpython-and-threads/
You cannot interact with the GUI from another thread. Your callback should work like this:
def OnRun(self, event):
<get any data you need from the GUI>
thread.start_new_thread(self.WorkerThread, (parameter1, parameter2, ...))
def WorkerThread(self, parameter1, parameter2, ...):
# do time-consuming work here. To send data to
# the GUI use CallAfter:
wx.CallAfter(self.textOutput.AppendText, "whatever")
The key is to do all the non-time-consuming GUI interaction in the main thread before spawning the worker thread. Once the worker thread starts it should have all the data it needs. It can then proceed to do its work, and use CallAfter to communicate back to the main thread.

Resources