Just for fun, I am implementing a python program to generate Ulam spirals (https://en.wikipedia.org/wiki/Ulam_spiral) of arbitrary size. The program receives the size of the spiral and eventually the program prints the spiral on stdout.
I was looking at how to improve the display of the spiral on the terminal, especially if the spiral is very large or the terminal window is a bit short, causing characters to overlap and break the image. I found out that ncourses (https://en.wikipedia.org/wiki/Ncurses) can be used for this, as it can create scrollable pads inside the windows that should not overlap.
With this in mind, I am thinking of a layout like this:
|--------------------------------------|--------------------------------------|
| Application logs | Spiral goes here |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
|--------------------------------------|--------------------------------------|
The "Application logs" are implemented with the python logging library, with a custom handler that makes the ads and refreshes the pad itself. So far, the code I have for this:
main.py
import logging
import curses
from curses import wrapper
from utils.log_handler import CursesHandler
import signal
##logger config
log = logging.getLogger()
log.setLevel(logging.getLevelName('INFO'))
log_formatter = logging.Formatter("%(asctime)s [%(processName)s]-[%(levelname)s] [%(threadName)s]: %(message)s")
def start_cli_windows(stdscr):
current_windows = []
height,width = stdscr.getmaxyx()
middle_columns = width // 2
ulam_spiral_pad = curses.newpad(height, middle_columns)
ulam_spiral_pad.addstr(0, 0, "*** SPIRAL GOES HERE ***")
##actual spiral on the right, from the middle to the end
ulam_spiral_pad.refresh(0, 0, 0, middle_columns ,height, width)
##set up logger, with the position for the logger
console_handler = CursesHandler(screen_top_left_col=0,screen_top_left_row=0,screen_bottom_right_col=middle_columns,screen_bottom_right_row=height)
console_handler.setFormatter(log_formatter)
log.addHandler(console_handler)
##Add our own handler as a window for our application
current_windows.append(console_handler)
## let's do some testing
for i in range(1000):
log.info(f"This is the log number {i}")
console_handler.poll_for_input()
def handler(signum, frame):
#just quit, the wrapper will clean up
exit(0)
if __name__ == "__main__":
signal.signal(signal.SIGINT, handler)
wrapper(start_cli_windows)
utils/log_handler.py
import logging
import curses
from utils.curses_wrapper import CursesWindow
class CursesHandler(logging.Handler, CursesWindow):
"""
Class that overrides a Logging handler, so it will point to a curses window instead of a regular
terminal or file. The class will create the padding at the specified parameters, and also will scroll if required
"""
def __init__(self, screen_top_left_col,screen_top_left_row,screen_bottom_right_col,screen_bottom_right_row):
logging.Handler.__init__(self)
CursesWindow.__init__(self, screen_top_left_col,screen_top_left_row,screen_bottom_right_col,screen_bottom_right_row)
def emit(self, record):
try:
msg = self.format(record)
self._screen.addstr(f"\n{msg}")
self._refresh_screen()
except (KeyboardInterrupt, SystemExit):
raise
except Exception as e:
self.handleError(record)
utils/curses_wrapper.py
import curses
class CursesWindow():
def __init__(self,screen_top_left_col,screen_top_left_row,screen_bottom_right_col,screen_bottom_right_row):
self._screen = curses.newpad(screen_bottom_right_row,screen_bottom_right_col)
self._screen.scrollok(True)
self._screen.keypad(1)
# The upper left corner of the pad region to be displayed are set by the following parameters. As this is new,
# init with 0, but scrolling may change this
self._min_row_to_display = 0
self._min_col_to_display = 0
# clipping box on the screen within which the pad region is to be displayed
self._screen_top_left_col = screen_top_left_col
self._screen_top_left_row = screen_top_left_row
self._screen_bottom_right_col = screen_bottom_right_col
self._screen_bottom_right_row = screen_bottom_right_row
#property
def min_row_to_display(self):
return self._min_row_to_display
#property
def min_col_to_display(self):
return self._min_col_to_display
#property
def window_position(self):
return (self._screen_top_left_col,self._screen_top_left_row,self._screen_bottom_right_col,self._screen_bottom_right_row)
#window_position.setter
def set_window_position(self, position_tuple):
self._screen_top_left_col,self._screen_top_left_row,self._screen_bottom_right_col,self._screen_bottom_right_row = position_tuple
self._refresh_screen()
def _refresh_screen(self):
self._screen.refresh(self._min_row_to_display,
self._min_col_to_display,
self._screen_top_left_row,
self._screen_top_left_col,
self._screen_bottom_right_row,
self._screen_bottom_right_col)
def scroll_window_up(self):
self._screen.scroll(-1)
self._refresh_screen()
def scroll_window_down(self):
self._screen.scroll(1)
self._refresh_screen()
def poll_for_input(self):
QUIT_PROGRAM = False
while not QUIT_PROGRAM:
c = self._screen.getch()
if c == ord('q'):
QUIT_PROGRAM = True
elif c == curses.KEY_UP:
self.scroll_window_up()
elif c == curses.KEY_DOWN:
self.scroll_window_down()
This code kinds of works, as it will let me add strings, and auto scroll when the end of the space is reached. However, when I try to scroll, it kinds of removes the data that is there. Showing gif as an example:
As you can see, the lines are lost forever once the scroll moves. My hypothesis is that the refresh function repaints the viewport, however, it is not painting the content that is inside the pad. A possible reason is that I am telling the refresh to always show the coordinates (0,0) when in reality I should use the line number I want the scroll start from.
Most of the samples I saw online assume the number of lines to show is known, either because the data is on an array, or there is a way to calculate the number beforehand. That way, the new position, and refresh can be done accurately. However, in this case, I don't know how many lines will be on the log, as I just receive the stream of data to print, and then add it to the pad.
So, the question: Is there a way to get how many lines are printed on the pad? Lines can be wrapped, so one line not necessarily correlates to a one-log message in this case. I tried the following approaches:
Using window.getyx(), which returns the position of the cursor relative to the pad but it returns the height of the component when the cursor is at the button. For example, I expected to return 1000 (as those are the number of lines on the pad) but it returns 100 (the number of lines of my terminal)
Using window.getyx(), however, it returns the boundaries of the pad, not the data.
If needed, I can implement a circular buffer or something that will hold the data and use that as a backend, I think. However, first I want to know if I am missing something here.
Thanks in advance for the help!
Related
I'm loading a mysql database on a tablewidget.
I have two functions which are connected to the item changed signal in the my QTable Widget. The first makes a list of the changed cells while the second is used to manage the data types input in the table widget cells bringing up an error message if the wrong datatype is put in.
The problem is that the functions work before I load my database in. Effectively storing a list of cells I don't want and popping continuous error messages while it is loading.
How do I halt the functions working till after the database is loaded?
def log_change(self, item):
self.changed_items.append([item.row(),item.column()])
def item_changed(self, Qitem,item):
if (item.column())%2 == 0:
try:
test = float(Qitem.text())
except ValueError:
Msgbox = QMessageBox()
Msgbox.setText("Error, value must be number!")
Msgbox.exec()
Qitem.setText(str(''))
There are 2 options:
Use a flag:
# in constructor
self.is_loading = False
def load_from_db(self):
# in load function
self.is_loading = True
# load from db
# ...
self.is_loading = False
# foo_callback is item_changed by example
def foo_callback(self, arg1, arg2, ...):
if self.is_loading:
return
# do work
Use blockSignals
self.tablewidget.blockSignals(True)
# load from db
# ...
self.tablewidget.blockSignals(False)
I've defined a sikuli module which is used to click on an image when something appears inside a region.
# observer.py
from sikuli import *
class Observer:
# When "observedImage" appears inside "region", double click on "reactImage"
def __init__(self, region, observedImage, reactImage):
self.region = region
self.observedImage = observedImage
self.reactImage = reactImage
def start(self):
self.region.onAppear(self.observedImage, self.appearHandler)
self.region.observe(FOREVER, background = True)
def appearHandler(self, event):
doubleClick(self.reactImage)
event.repeat()
def stop(self):
self.region.stopObserver()
Here's how to use it:
import observer
import time
observer.Observer(Region(111,222,333,444), "imageToBeDetected1.png", "imageToBeClicked1.png").start()
observer.Observer(Region(555,666,66,666), "imageToBeDetected2.png", "imageToBeClicked2.png").start()
while True:
print('waiting')
time.sleep(1)
The problem with the above code is that when imageToBeDetected1 and imageToBeDetected2 both appear in Region(111,222,333,444) and Region(555,666,66,666) respectively, my mouse will move between imageToBeClicked1 and imageToBeClicked2. I want only imageToBeDetected1 to be clicked in this situation.
imageToBeDetected2 should be ignored when imageToBeDetected1 and imageToBeDetected2 both appear in Region(111,222,333,444) and Region(555,666,66,666), respectively.
How can I modify my code so that imageToBeDetected1 has a higher priority over imageToBeDetected2?
Or is there a better way to observe multiple images with sikuli?
I have a QGraphicsScene containing some simple objects (in this simplified example circles) that I want to change into other objects (here squares) when selected. More specifically I'd like to have parent objects which don't draw themselves, they are drawn by their child objects, and under various circumstances, but in particular when the parent objects are selected, I'd like the set of child objects to change. This is a nice conceptual framework for the overall app I am working on.
So I've implemented this in PySide and I thought it was working fine: the circles change nicely into squares when you click on them.
Until I use RubberBandDrag selection in the view. This causes an instant segfault when the rubber band selection reaches the parent object and the selection changes. Presumably this is being triggered because the rubber band selection in QT is somehow keeping a pointer to the child item which is disappearing before the rubber band selection action is complete.
Simplified code below - test it by first clicking on the object (it changes nicely) then dragging over the object - segfault:
from PySide import QtCore,QtGui
class SceneObject(QtGui.QGraphicsItem):
def __init__(self, scene):
QtGui.QGraphicsItem.__init__(self, scene = scene)
self.setFlag(QtGui.QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QtGui.QGraphicsItem.ItemHasNoContents, True)
self.updateContents()
def updateContents(self):
self.prepareGeometryChange()
for c in self.childItems():
self.scene().removeItem(c)
if self.isSelected():
shape_item = QtGui.QGraphicsRectItem()
else:
shape_item = QtGui.QGraphicsEllipseItem()
shape_item.setFlag(QtGui.QGraphicsItem.ItemIsSelectable, False)
shape_item.setFlag(QtGui.QGraphicsItem.ItemStacksBehindParent,True)
shape_item.setPen(QtGui.QPen("green"))
shape_item.setRect(QtCore.QRectF(0,0,10,10))
shape_item.setParentItem(self)
def itemChange(self, change, value):
if self.scene() != None:
if change == QtGui.QGraphicsItem.ItemSelectedHasChanged:
self.updateContents()
return
return super(SceneObject,self).itemChange(change, value)
def boundingRect(self):
return self.childrenBoundingRect()
class Visualiser(QtGui.QMainWindow):
def __init__(self):
super(Visualiser,self).__init__()
self.viewer = QtGui.QGraphicsView(self)
self.viewer.setDragMode(QtGui.QGraphicsView.RubberBandDrag)
self.setCentralWidget(self.viewer)
self.viewer.setScene(QtGui.QGraphicsScene())
parent_item = SceneObject(self.viewer.scene())
parent_item.setPos(50,50)
app = QtGui.QApplication([])
mainwindow = Visualiser()
mainwindow.show()
app.exec_()
So questions:
Have I just made a mistake that can be straightforwardly fixed?
Or is removing objects from the scene not allowed when handling an ItemSelectedHasChanged event?
Is there a handy workaround? Or what's a good alternative approach? I could replace the QGraphicsRectItem with a custom item which can be drawn either as a square or a circle but that doesn't conveniently cover all my use cases. I can see that I could make that work but it will certainly not be as straightforward.
EDIT - Workaround:
It is possible to prevent this failing by preserving the about-to-be-deleted object for a while. This can be done by something like this:
def updateContents(self):
self.prepareGeometryChange()
self._temp_store = self.childItems()
for c in self.childItems():
self.scene().removeItem(c)
...
However, this is ugly code and increases the memory usage for no real benefit. Instead I have moved to using the QGraphicsScene.selectionChanged signal as suggested in this answer.
I've debugged it. Reproduced on Lunix
1 qFatal(const char *, ...) *plt 0x7f05d4e81c40
2 qt_assert qglobal.cpp 2054 0x7f05d4ea197e
3 QScopedPointer<QGraphicsItemPrivate, QScopedPointerDeleter<QGraphicsItemPrivate>>::operator-> qscopedpointer.h 112 0x7f05d2c767ec
4 QGraphicsItem::flags qgraphicsitem.cpp 1799 0x7f05d2c573b8
5 QGraphicsScene::setSelectionArea qgraphicsscene.cpp 2381 0x7f05d2c94893
6 QGraphicsView::mouseMoveEvent qgraphicsview.cpp 3257 0x7f05d2cca553
7 QGraphicsViewWrapper::mouseMoveEvent qgraphicsview_wrapper.cpp 1023 0x7f05d362be83
8 QWidget::event qwidget.cpp 8374 0x7f05d2570371
qt-everywhere-opensource-src-4.8.6/src/gui/graphicsview/qgraphicsscene.cpp:2381
void QGraphicsScene::setSelectionArea(const QPainterPath &path, Qt::ItemSelectionMode mode,
const QTransform &deviceTransform)
{
...
// Set all items in path to selected.
foreach (QGraphicsItem *item, items(path, mode, Qt::DescendingOrder, deviceTransform)) {
if (item->flags() & QGraphicsItem::ItemIsSelectable) { // item is invalid here
if (!item->isSelected())
changed = true;
unselectItems.remove(item);
item->setSelected(true);
}
}
They are using items() function to find a list of items under the rubber band selection. But if one item while processing deletes something the item pointer just becomes invalid. And next call to item->flags() causes the crash.
As alternative you could use QGraphicsScene::selectionChanged signal. It's emitted only once per selection change.
Looks like it's not expected by Qt to have some major changes in itemChange
Behind of this here is common mistake you have with prepareGeometryChange() call.
It's designed to be called right before changing boundingRect. Bounding rect should be the old one when prepareGeometryChange called and new one right after.
So that's could happen:
In updateContents:
self.prepareGeometryChange(); # calls boundingRect. old value returned
...
shape_item.setParentItem(self); # could call the boundingRect. but still old value returned!
After child added it calls boundingRect again but value unexpected different.
As a solution you can add a variable
def updateContents(self):
for c in self.childItems():
self.scene().removeItem(c)
if self.isSelected():
shape_item = QtGui.QGraphicsRectItem()
else:
shape_item = QtGui.QGraphicsEllipseItem()
shape_item.setFlag(QtGui.QGraphicsItem.ItemIsSelectable, False)
shape_item.setFlag(QtGui.QGraphicsItem.ItemStacksBehindParent,True)
shape_item.setPen(QtGui.QPen("green"))
shape_item.setRect(QtCore.QRectF(0,0,10,10))
shape_item.setParentItem(self)
self.prepareGeometryChange();
self._childRect = self.childrenBoundingRect()
def boundingRect(self):
return self._childRect
AVbin is installed. Both .wav and .mp3 files work.
import pyglet
music = pyglet.media.load('A.mp3')
music.play()
player = pyglet.media.Player()
player.queue( pyglet.media.load('B.mp3'))
player.queue( pyglet.media.load('C.wav'))
player.play()
pyglet.app.run()
pyglet.app.exit()
I want to create a program that plays A, then plays the queue with B and then C, and finally quits after all three sounds play.
I tried the code above but according to this post, "this is [solely] because app.run() is a never-ending loop."
How can I modify my code minimally so that the program quits after the three sounds are played?
Bonus, but how can I modify my code minimally so that the program can play two (or more) sound files, E.mp3 and F.mp3, at once?
Thanks!
Because what you're asking is not as simple as you'd might think it is.
I've put together a code example with as much comments as I possibly could fit in without making the example to hard to read.
Below the code, I'll try to explain a few key functions as detailed as possible.
import pyglet
from pyglet.gl import *
from collections import OrderedDict
key = pyglet.window.key
class main(pyglet.window.Window):
def __init__ (self, width=800, height=600, fps=False, *args, **kwargs):
super(main, self).__init__(width, height, *args, **kwargs)
self.keys = OrderedDict() # This just keeps track of which keys we're holding down. In case we want to do repeated input.
self.alive = 1 # And as long as this is True, we'll keep on rendering.
## Add more songs to the list, either here, via input() from the console or on_key_ress() function below.
self.songs = ['A.wav', 'B.wav', 'C.wav']
self.song_pool = None
self.player = pyglet.media.Player()
for song in self.songs:
media = pyglet.media.load(song)
if self.song_pool is None:
## == if the Song Pool hasn't been setup,
## we'll set one up. Because we need to know the audio_format()
## we can't really set it up in advance (consists more information than just 'mp3' or 'wav')
self.song_pool = pyglet.media.SourceGroup(media.audio_format, None)
## == Queue the media into the song pool.
self.song_pool.queue(pyglet.media.load(song))
## == And then, queue the song_pool into the player.
## We do this because SourceGroup (song_pool) as a function called
## .has_next() which we'll require later on.
self.player.queue(self.song_pool)
## == Normally, you would do self.player.eos_action = self.function()
## But for whatever windows reasons, this doesn't work for me in testing.
## So below is a manual workaround that works about as good.
self.current_track = pyglet.text.Label('', x=width/2, y=height/2+50, anchor_x='center', anchor_y='center')
self.current_time = pyglet.text.Label('', x=width/2, y=height/2-50, anchor_x='center', anchor_y='center')
def on_draw(self):
self.render()
def on_close(self):
self.alive = 0
def on_key_release(self, symbol, modifiers):
try:
del self.keys[symbol]
except:
pass
def on_key_press(self, symbol, modifiers):
if symbol == key.ESCAPE: # [ESC]
self.alive = 0
elif symbol == key.SPACE:
if self.player.playing:
self.player.pause()
else:
self.player.play()
elif symbol == key.RIGHT:
self.player.seek(self.player.time + 15)
## == You could check the user input here,
## and add more songs via the keyboard here.
## For as long as self.song_pool has tracks,
## this player will continue to play.
self.keys[symbol] = True
def end_of_tracks(self, *args, **kwargs):
self.alive=0
def render(self):
## Clear the screen
self.clear()
## == You could show some video, image or text here while the music plays.
## I'll drop in a example where the current Track Name and time are playing.
## == Grab the media_info (if any, otherwise this returns None)
media_info = self.player.source.info
if not media_info:
## == if there were no meta-data, we'll show the file-name instead:
media_info = self.player.source._file.name
else:
## == But if we got meta data, we'll show "Artist - Track Title"
media_info = media_info.author + ' - ' + media_info.title
self.current_track.text = media_info
self.current_track.draw()
## == This part exists of two things,
## 1. Grab the Current Time Stamp and the Song Duration.
## Check if the song_pool() is at it's end, and if the track Cur>=Max -> We'll quit.
## * (This is the manual workaround)
cur_t, end_t = int(self.player.time), int(self.player.source._get_duration())
if self.song_pool.has_next() is False and cur_t >= end_t:
self.alive=False
## 2. Show the current time and maximum time in seconds to the user.
self.current_time.text = str(cur_t)+'/'+str(end_t) + 'seconds'
self.current_time.draw()
## This "renders" the graphics:
self.flip()
def run(self):
while self.alive == 1:
self.render()
# -----------> This is key <----------
# This is what replaces pyglet.app.run()
# but is required for the GUI to not freeze
#
event = self.dispatch_events()
x = main()
x.run()
Now, normally you'd decorate your way trough this with a bunch of functions.
But I like to subclass and OOP my way through any graphical libraries, because it gets messy quite fast otherwise.
So instead of pyglet.app.run(), I've got a custom made run() function.
All this does is mimic the pyglet.app.run(), for the most part. Enough to get going at least.
Because player.eos_* events appears to be broken.
I've added a manual example of how you could check if the songs are done playing or not.
This is a combination of self.song_pool pyglet.media.SourceGroup, self.player.time pyglet.media.player.time and self.player.source._get_duration() which returns the track duration.
The SourceGroup gives us a has_next() function which tells us if we're at the end of the queued songs. The other two variables tells us if we've reached the end of the current track. This is all we need to determinate if we want to exit or not.
Now, I haven't technically added a way to add more songs. Because again, that would also be harder than you think. Unless you opt in for if symbol == key.LCTRL: self.song_pool.queue(pyglet.media.load(input('Song: '))) for instance. But again, all you would need to do, is add more songs to the self.song_pool queue, and there you go.
I hope this answers your question. Even the bonus one.
I have a program that uses pyqt's .animateClick() feature to show the user a sequence of different button clicks that the user has to copy in that specific order. The problem is I don't want the animateClick() to send a signal, I only want the button click signals from the user. Here is some of my code to demonstrate what I mean, and how I tried to solve that problem (that doesn't work). I simplified my code quite a bit so its easier to read, let me know if you have any questions.
from PyQt4 import QtCore,QtGui
global flag
global ai_states
ai_states = []
user_states = []
class Program(object):
# Set up the push buttons
#Code Here.
# Connect push buttons to function get_state()
self.pushButton.clicked.connect(self.get_state)
self.pushButton_2.clicked.connect(self.get_state)
self.pushButton_3.clicked.connect(self.get_state)
self.pushButton_4.clicked.connect(self.get_state)
# Code that starts the start() function
def start(self):
flag = 0
ai_states[:] = []
i = -1
# Code here that generates ai_states, numbers 1-4, in any order, based on button numbers.
for k in ai_states:
i = i + 1
# Code here that animates button clicks determined by ai_states
# Changes the flag to 1 once the loop ends
if i == len(ai_states):
flag = 1
def get_state(self):
if flag == 1:
user_states.append(str(self.centralWidget.sender().text()))
else:
pass
if len(user_states) == len(ai_states):
# Checks to make sure the user inputted the same clicks as the ai_states
Even though the flag does come out to be 1 after the start() function, it is still appending the animatedClick() signals. What am I doing wrong? I'm new to GUI programming, so I'm probably going about this in a very bad way. Any help would be appreciated.
Never use global variables unless you really have to. If you need shared access to variables, use instance attributes:
from PyQt4 import QtCore,QtGui
class Program(object):
def __init__(self):
self.ai_states = []
self.user_states = []
self.flag = 1
# Set up the push buttons
# Code Here
# Connect push buttons to function get_state()
self.pushButton.clicked.connect(self.get_state)
self.pushButton_2.clicked.connect(self.get_state)
self.pushButton_3.clicked.connect(self.get_state)
self.pushButton_4.clicked.connect(self.get_state)
# Code that starts the start() function
def start(self):
self.flag = 0
del self.ai_states[:]
i = -1
# Code here that generates ai_states, numbers 1-4, in any order, based on button numbers.
for k in self.ai_states:
i = i + 1
# Code here that animates button clicks determined by ai_states
# Changes the flag to 1 once the loop ends
self.flag = 1
def get_state(self):
if self.flag == 1:
self.user_states.append(str(self.centralWidget.sender().text()))
if len(self.user_states) == len(self.ai_states):
# Checks to make sure the user inputted the same clicks as the ai_states