Gtk3 event for noticing when a GtkPaned's split changes - haskell

I'm making a small application, and I'd like to save the size and position of the various adjustable-sized widgets so that when the application is started the next time, the user sees the same thing as when they last quit. I'm having a bit of trouble working out what event I should listen for to know when widgets' sizes are adjusted. Looking around online, it seems like configure-event is the right one, but in my test this event doesn't fire when widgets are resized by the user dragging a GtkPaned's split. I've included a minimal example below.
(I know this is a Haskell program, but I tried to avoid any of the fancy features so that it would be readable even by non-Haskell experts. The main thing you might not be able to guess yourself when reading it is that function applications are done with just a space, so what would be f(x, y, z) in most other languages is f x y z in Haskell.)
import Control.Monad.IO.Class
import Graphics.UI.Gtk
main :: IO ()
main = do
initGUI
window <- windowNew
on window objectDestroy mainQuit
eventLog <- labelNew (Just "")
-- For this minimal example, the widgets have no content. But let's
-- pretend by just asking for a little space which we'll leave blank.
widgetSetSizeRequest eventLog 200 200
uiGraph <- labelNew (Just "")
widgetSetSizeRequest uiGraph 200 200
dataPane <- vPanedNew
panedPack1 dataPane eventLog True True
panedPack2 dataPane uiGraph False True
-- Create a callback to print the position of the pane's split. We'll
-- always say that we didn't end up handling the event and that it should
-- get propagated to wherever it used to go by returning False.
let report = liftIO $ do
print =<< panedGetPosition dataPane
return False
on window configureEvent report
on eventLog configureEvent report
on uiGraph configureEvent report
on dataPane configureEvent report
containerAdd window dataPane
widgetShowAll window
mainGUI
When I run this program, the position of the pane's split is printed when the window is moved around or resized, but not when the split itself is moved.
Is there another event I can listen to that would tell me when that was happening? How can I know when the user is customizing the view so that I can save that information to disk?

You can connect to changes of GObject's property using "notify::{param-name-here}" signal. In your case it's the position property.
That's how it's done with python:
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
def position(paned, param):
print(paned.get_position()) # either call object's method
print(paned.get_property(param.name)) # or obtain property value by name
win = Gtk.Window()
paned = Gtk.Paned()
paned.connect("notify::position", position)
paned.pack1(Gtk.Label.new("abc"))
paned.pack2(Gtk.Label.new("def"))
win.add(paned)
win.show_all()
Gtk.main()
For the Haskell analog, you can use notifyProperty to get a signal for a given attribute. Although there is a panedPosition attribute with the right type, it doesn't work, because it's not implemented as a named attribute under the hood (not sure why). Instead, you have to build the attribute from raw materials yourself, using newAttrFromIntProperty. Putting the pieces together, you can add this line to get a report every time the split is moved:
on dataPane (notifyProperty (newAttrFromIntProperty "position")) report
(The existing configureEvent lines from the question are not needed.)

Related

ttk Treeview, get item's bbox after scrolling into view

I am working on an editable tkinter.ttk.Treeview subclass. For editing I need to place the edit widget on top of a choosen "cell" (list row/column). To get the proper coordinates, there is the Treeview.bbox() method.
If the row to be edited is not in view (collapsed or scrolled away), I cannot get its bbox obviously. Per the docs, the see() method is meant to bring an item into view in such a case.
Example Code:
from tkinter import Tk, Button
from tkinter.ttk import Treeview
root = Tk()
tv = Treeview(root)
tv.pack()
iids = [tv.insert("", "end", text=f"item {n}") for n in range(20)]
# can only get bbox once everything is on screen.
n = [0]
def show_bbox():
n[0] += 1
iid = iids[n[0]]
b = tv.bbox(iid)
if not b:
# If not visible, scroll into view and try again
tv.see(iid)
# ... but this still doesn't return a valid bbox!?
b = tv.bbox(iid)
print(f"bbox of item {n}", b)
btn = Button(root, text="bbox", command=show_bbox)
btn.pack(side="bottom")
root.mainloop()
(start, then click the button until you reach an invisible item)
The second tv.bbox() call ought to return a valid bbox, but still returns empty string. Apparently see doesnt work immediately, but enqeues the viewport change into the event queue somehow. So my code cannot just proceed synchronously as it seems.
How to solve this? Can see() be made to work immediately? If not, is there another workaround?
The problem is that even after calling see, the item isn't visible (and thus, doesn't have a bounding box) until it is literally drawn on the screen.
A simple solution is to call tv.update_idletasks() immediately after calling tv.see(), which should cause the display to refresh.
Another solution is to use tv.after to schedule the display of the box (or the overlaying of an entry widget) to happen after mainloop has a chance to refresh the window.
def print_bbox(iid):
bbox = tv.bbox(iid)
print(f"bbox of item {iid}", bbox)
def show_bbox():
n[0] += 1
iid = iids[n[0]]
tv.see(iid)
tv.after_idle(print_bbox, iid)

Waiting Screen while data is being extracted from an API

I have created a Python Script to download data using an API. And I have put a simple GUI on top of it as well using PySimpleGUI.
However, while the data is being downloaded, I want to show a indeterminate progressbar or something like that, which will exit on its own after the download completes.
Is there any way to implement this requirement?
Two ways easily for it, element sg.ProgressBar or simple sg.Text with different length of string, maybe █, to show the state of progress.
Demo_Progress_Meters
or
from random import randint
import PySimpleGUI as sg
sg.theme('DarkBlue')
layout = [[sg.Text('', size=(50, 1), relief='sunken', font=('Courier', 11),
text_color='yellow', background_color='black',key='TEXT')]]
window = sg.Window('Title', layout, finalize=True)
text = window['TEXT']
state = 0
while True:
event, values = window.read(timeout=100)
if event == sg.WINDOW_CLOSED:
break
state = (state+1)%51
text.update('█'*state)
window.close()
Note: remember to use monospaced font, otherwise the length of sg.Text will be different as length of state string.
Depend on progress of job to set state of progress.

frozen window during heavy operation

im trying to load some files from folders and i want to show the progress of it with progress bar. At this point i tried multiprocessing, timers, threads and nothing is working... I dont know why my code is not working.
after run this code it shows me basic window but after line where i want to compare files, which is heavy operation, but im calling refresh of prograss bar here and in my console i see that it is working but the window with progress bar is frozen the whole time and never refresh :(( (Not responding)
#here is box with progress bar
self.ex[0]=showLoadingBox(len(files[1]))
#this is the heavy operation that i want to show the progress
duplsCompare=self.comPictures(files, sett.qualityMeasure)
#---------------------------------------------------------------------------
def comPictures(self,ignSource, prepQ):
dp = []
ret = []
source = ignSource[1]
for index, item in enumerate(source):
#here im trying to refresh progress bar
reload(self.ex[0],index+1)
for p in source[index + 1:]:
#do some stuff with files...
#return comparedFiles
# --------------------------------------------------------------------------------------------reloading method
def reload(self,load):
if(not(self.count==0)):
val=int((load/self.count)*100)
else:
val=100
self.pBar.setValue(val)
print(val)
if(not(load<self.count)):
self.close()
After end of comparing the window start working but that worthless for me i need that prograss bar show the prograss of comparing.
UPDATE:
so i use threading module but still no progress here literally. Window with progress bar just frozen and show me no progress and not reponding still the same problem even with different thread...
retComPic=[]
t=Thread(target=self.comPictures,args=(files, sett.qualityMeasure,retComPic))
t.daemon = True
t.start()
while(not(len(retComPic))):
reload(self.ex[0])
t.join()
# self.ex[0].close()
duplsCompare=retComPic[0]
The result is the same percentage right in the console but progress window frozen

PyQt QScrollArea doesn't display widgets

I am somewhat new to GUI programming and very new to PyQt, and I'm trying to build a GUI that displays a list of questions. I have created a QuestionBank class that subclasses QWidget and overrides the .show() method to display the list properly. I have tested this alone and it works correctly. However, the list of questions can be quite long, so I've been trying to make it scrollable. Rather than add a QScrollBar to the widget and then set up the event triggers by hand, I've been trying to my QuestionBank widget in a QScrollArea based on the syntax I've seen in examples online. While the scroll area shows up fine, it does not at all display the question bank but rather just shows a blank outline.
The QuestionBank class looks like this:
class QuestionBank(QWidget):
BUFFER = 10 # space between questions (can be modified)
def __init__(self, parent, questions):
# `parent` should be the QWidget that contains the QuestionBank, or None if
# QuestionBank is top level
# `questions` should be a list of MasterQuestion objects (not widgets)
QWidget.__init__(self, parent)
self.questions = [MasterQuestionWidget(self, q) for q in questions]
self.bottomEdge = 0
def show(self, y=BUFFER):
QWidget.show(self)
for q in self.questions:
# coordinates for each each question
q.move(QuestionBank.BUFFER, y)
q.show()
# update y-coordinate so that questions don't overlap
y += q.frameGeometry().height() + QuestionBank.BUFFER
self.bottomEdge = y + 3 * QuestionBank.BUFFER
# ... other methods down here
My code for showing the scroll bar looks like this:
app = QApplication(sys.argv)
frame = QScrollArea()
qs = QuestionBank(None, QFileManager.importQuestions())
qs.resize(350, 700)
frame.setGeometry(0, 0, 350, 300)
frame.setWidget(qs)
frame.show()
sys.exit(app.exec_())
I have tried many variants of this, including calling resize on frame instead of qs, getting rid of setGeometry, and setting the parent of qs to frame instead of None and I have no idea where I'm going wrong.
If it helps, I'm using PyQt5
Here is the question bank without the scroll area, to see what it is supposed to look like:
Here is the output of the code above with the scroll area:
This variation on the code is the only one that produces any output whatsoever, the rest just have blank windows. I'm convinced its something simple I'm missing, as the frame is obviously resizing correctly and it obviously knows what widget to display but its not showing the whole thing.
Any help is much appreciated, thank you in advance.

Continually running functions in tkinter after mainloop in Python 3.4

What I want to do is colour in a single pixel in the centre of the screen, then at random choose an adjacent pixel and colour it in, and then repeat until some condition is met - anything such as time, or the screen is full, or after a certain number of pixels are full. This ending isn't too important, I haven't got that far yet, and I think I could manage to work that out myself.
I have no experience with tkinter, but I decided it was the best way to display this, since I don't really no any other way. Some of this code (mainly the tkinter functions like Canvas, PhotoImage etc) is therefore copy-pasted (and slightly edited) from examples I found here.
What my code does when run is hard to tell - it uses the CPU as much as it can seemingly indefinitely, and slowly increases its memory usage, but doesn't appear to do anything. No window opens, and the IDLE interpreter goes to a blank line, as usual when calculating something. When killed, the window opens, and displays a white page with a little black blob in the bottom right corner - as if the program had done what it was meant to, but without showing it happening, and starting in the wrong place.
So:
Why does it do this?
What should I do to make my program work?
What would be a better way of coding this, changing as many things as you like (ie. no tkinter, a different algorithm etc)?
from tkinter import Tk, Canvas, PhotoImage, mainloop
from random import randrange
from time import sleep
def choose_pixel(pixel_list):
possible_pixels = []
for x in pixel_list:
#adjacent pixels to existing ones
a = [x[0] + 1, x[1]]
b = [x[0] - 1, x[1]]
c = [x[0], x[1] + 1]
d = [x[0], x[1] - 1]
#if a not in pixel_list:
possible_pixels.append(a)
#if b not in pixel_list:
possible_pixels.append(b)
#if c not in pixel_list:
possible_pixels.append(c)
#if d not in pixel_list:
possible_pixels.append(d)
pixel_choosing = randrange(len(possible_pixels))
final_choice = possible_pixels[pixel_choosing]
return final_choice
def update_image(img_name, pixel):
img.put("#000000", (pixel[0], pixel[1]))
WIDTH, HEIGHT = 320, 240
window = Tk()
#create white background image
canvas = Canvas(window, width=WIDTH, height=HEIGHT, bg="#ffffff")
canvas.pack()
img = PhotoImage(width=WIDTH, height=HEIGHT)
canvas.create_image((WIDTH, HEIGHT), image=img, state="normal")
first_pixel = [int(WIDTH/2), int(HEIGHT/2)]
pixel_list = [first_pixel]
img.put("#000000", (first_pixel[0], first_pixel[1]))
canvas.pack()
runs = 0
while True:
next_pixel = choose_pixel(pixel_list)
pixel_list.append(next_pixel)
window.after(0, update_image, img, next_pixel)
canvas.pack()
runs+=1
window.mainloop()
The pattern for running something periodically in tkinter is to write a function that does whatever you want it to do, and then the last thing it does is use after to call itself again in the future. It looks something like this:
import tkinter as tk
...
class Example(...):
def __init__(self, ...):
...
self.canvas = tk.Canvas(...)
self.delay = 100 # 100ms equals ten times a second
...
# draw the first thing
self.draw_something()
def draw_something(self):
<put your code to draw one thing here>
self.canvas.after(self.delay, self.draw_something)
After the function draws something, it schedules itself to run again in the future. The delay defines approximately how long to wait before the next call. The smaller the number, the faster it runs but the more CPU it uses. This works, because between the time after is called and the time elapses, the event loop (mainloop) is free to handle other events such as screen redraws.
While you may think this looks like recursion, it isn't since it's not making a recursive call. It's merely adding a job to a queue that the mainloop periodically checks.

Resources