Blog 2019/2/26
<- previous | index | next ->
Progress report for 2019/2/26
Here's a visual treatment for dictionaries (key/value pairs).
["hello", ["world", ["these", "are"]], {"a": 1, "b": 2}, "words", "in", "boxes"]
Blog 2019/2/26
<- previous | index | next ->
Progress report for 2019/2/26
Here's a visual treatment for dictionaries (key/value pairs).
["hello", ["world", ["these", "are"]], {"a": 1, "b": 2}, "words", "in", "boxes"]
| #!/usr/bin/env python | |
| # a pyqt script which draws words inside of rounded rectangles. | |
| # see https://doc.qt.io/qt-5/qpainter.html | |
| # see http://pyqt.sourceforge.net/Docs/PyQt5/api/qpen.html | |
| import sys | |
| import time | |
| from PyQt5.QtWidgets import * | |
| from PyQt5.QtCore import * | |
| from PyQt5.QtGui import * | |
| json_root = ["hello", ["world", ["these", "are"]], {"a": 1, "b": 2}, "words", "in", "boxes"] | |
| # given an rgb triple, return s QColor | |
| def qcolor(rgb): | |
| (r,g,b) = rgb | |
| return QColor(r,g,b,255) | |
| # return a lighter version of a color | |
| def lighter(rgb): | |
| (r,g,b) = rgb | |
| return ( | |
| min(255, int(r + ((255-r)/1.15))), | |
| min(255, int(g + ((255-g)/1.15))), | |
| min(255, int(b + ((255-b)/1.15))), | |
| ) | |
| colors = { | |
| "bg": (255,255,255), | |
| "word_border": (0,0,0), | |
| "word": (255,255,255), | |
| "array_border": (0,0,255), | |
| "array": lighter((0,0,255)), | |
| "dict_border": (255,0,0), | |
| "dict": lighter((255,0,0)) | |
| } | |
| # this function will be called on every paint event. | |
| def on_paint(event, painter): | |
| # fill the canvas with white | |
| painter.fillRect(event.rect(), QBrush(qcolor(colors["bg"]))) | |
| # paint the words | |
| (x, y) = (20, 20) | |
| paint_json(json_root, painter, x, y) | |
| # recursively paint a json structure, starting at (x,y). | |
| # returns the (width, height) used to paint the json structure. | |
| def paint_json(js, painter, x, y, pad=5): | |
| if isinstance(js, list): | |
| return paint_array(js, painter, x, y, pad) | |
| elif isinstance(js, dict): | |
| return paint_dictionary(js, painter, x, y, pad) | |
| else: | |
| return paint_word_in_box( | |
| "%s" % js, painter, x, y, colors["word"], colors["word_border"], pad | |
| ) | |
| # paint the items of the array in a container box. | |
| # returns the (width, height) used to paint the array. | |
| def paint_array(arr, painter, x, y, pad=5): | |
| # first, paint the container box | |
| (w, h) = size_of_json(arr, painter, pad) | |
| paint_box(painter, x, y, w, h, colors["array"], colors["array_border"]) | |
| # now paint the elements inside of the box | |
| y += pad | |
| for j in arr: | |
| x += pad | |
| (jw, jh) = size_of_json(j, painter, pad) | |
| # paint_json(j, painter, x, y + ((h-jh)/2.0), pad) | |
| paint_json(j, painter, x, y, pad) | |
| x += jw | |
| return (w, h) | |
| # paint the key-value pairs of the dictionary in a container box. | |
| # returns the (width, height) used to paint the dictionary. | |
| def paint_dictionary(d, painter, x, y, pad=5): | |
| # first, paint the container box | |
| (w, h) = size_of_json(d, painter, pad) | |
| paint_box(painter, x, y, w, h, colors["dict"], colors["dict_border"]) | |
| # now paint the key-value pairs inside the box | |
| y += pad | |
| for k in sorted(d.keys()): | |
| v = d[k] | |
| x += pad | |
| (iw, ih) = size_of_kv_pair((k,v), painter, pad) | |
| paint_kv_pair((k,v), painter, x, y, colors["word_border"], pad) | |
| x += iw | |
| return (w, h) | |
| # paint a linked key-value pair inside a linked pair of boxes. | |
| # returns the (width, height) used to paint the pair. | |
| def paint_kv_pair((k,v), painter, x, y, link_rgb, pad=5): | |
| (kw, kh) = size_of_json(k, painter, pad) | |
| (vw, vh) = size_of_json(v, painter, pad) | |
| paint_json(k, painter, x, y, pad) | |
| paint_json(v, painter, x + kw + pad, y, pad) | |
| # paint the link between the two boxes | |
| # configure the box stroke | |
| pen = QPen() | |
| pen.setColor(qcolor(link_rgb)) | |
| pen.setWidth(1) | |
| painter.setPen(pen) | |
| painter.drawLine( | |
| x + kw, y + (kh/2.0), | |
| x + kw + pad, y + (kh/2.0) | |
| ) | |
| # paint a word inside of a box, starting at (x, y). | |
| # returns the (width, height) used to paint the word. | |
| def paint_word_in_box(text, painter, x, y, rgb, border_rgb, pad=5): | |
| # calculate the size of the text | |
| text_bounds = QFontMetrics(painter.font()).boundingRect(text) | |
| w = pad + text_bounds.width() + pad | |
| h = pad + text_bounds.height() + pad | |
| paint_box(painter, x, y, w, h, rgb, border_rgb) | |
| # configure the text stroke | |
| pen = QPen() # defaults to black | |
| painter.setPen(pen) | |
| # draw the text | |
| text_top_fudge = -2 # qt text seems to have extra padding on top. | |
| baseline = y + text_bounds.height() + pad + text_top_fudge | |
| painter.drawText( | |
| pad + x, | |
| baseline, | |
| text | |
| ) | |
| return (w, h) | |
| # paint a box (with a border stroke and rounded corners) | |
| def paint_box(painter, x, y, w, h, rgb, border_rgb): | |
| corner_radius = 4 | |
| # configure the box stroke | |
| pen = QPen() | |
| pen.setColor(qcolor(border_rgb)) | |
| pen.setWidth(1) | |
| painter.setPen(pen) | |
| # configure the box fill | |
| painter.setBrush(QBrush(qcolor(rgb))) | |
| # draw the box | |
| # note: the rounded corners come out a bit odd. | |
| # see https://stackoverflow.com/questions/6507511/qt-round-rectangle-why-corners-are-different | |
| painter.drawRoundedRect( | |
| x, y, w, h, | |
| corner_radius, | |
| corner_radius | |
| ) | |
| # calculate the bounding box size of a json structure | |
| def size_of_json(js, painter, pad=5): | |
| if isinstance(js, list): | |
| total_w = pad | |
| total_h = 0 | |
| for j in js: | |
| (w, h) = size_of_json(j, painter, pad) | |
| total_w += (w + pad) | |
| total_h = max(total_h, h) | |
| return (total_w, pad + total_h + pad) | |
| elif isinstance(js, dict): | |
| total_w = pad | |
| total_h = 0 | |
| for k in sorted(js.keys()): | |
| v = js[k] | |
| (w, h) = size_of_kv_pair((k,v), painter, pad) | |
| total_w += (w + pad) | |
| total_h = max(total_h, h) | |
| return (total_w, pad + total_h + pad) | |
| else: | |
| return size_of_word_in_box("%s" % js, painter, pad) | |
| # calculate the bounding box size of one key-value pair | |
| def size_of_kv_pair((k,v), painter, pad=5): | |
| (kw, kh) = size_of_json(k, painter, pad) | |
| (vw, vh) = size_of_json(v, painter, pad) | |
| return ( | |
| kw + pad + vw, | |
| max(kh, vh) | |
| ) | |
| # calculate the bounding box size of a word in a box | |
| def size_of_word_in_box(text, painter, pad=5): | |
| text_bounds = QFontMetrics(painter.font()).boundingRect(text) | |
| w = pad + text_bounds.width() + pad | |
| h = pad + text_bounds.height() + pad | |
| return (w, h) | |
| # a widget which paints words inside of boxes | |
| class WordPainter(QWidget): | |
| def __init__(self, on_paint_fn): | |
| QWidget.__init__(self) | |
| self.on_paint_fn = on_paint_fn | |
| # this gets called every time the widget needs to repaint (e.g. window resize) | |
| def paintEvent(self, event): | |
| then = time.time() | |
| painter = QPainter() | |
| # hmm, these don't seem to do anything? | |
| # painter.setRenderHint(QPainter.TextAntialiasing, True) | |
| # painter.setRenderHint(QPainter.Antialiasing, True) | |
| painter.begin(self) | |
| self.on_paint_fn(event, painter) | |
| painter.end() | |
| now = time.time() | |
| elapsed = now - then | |
| print "elapsed: %s" % elapsed | |
| print "fps: %s" % (1.0/elapsed) | |
| if __name__ == "__main__": | |
| app = QApplication(sys.argv) | |
| window = WordPainter(on_paint) | |
| window.show() | |
| sys.exit(app.exec_()) | |
| # todo: | |
| # look into painting to a QImage, and then painting that with QPainter. | |
| # perhaps that can be a higher-performance "compositing" option? | |
| # (tradeoff less cpu for more memory usage?) | |
| # todo: | |
| # cache all of the calculated sizes of objects | |
| # note: this could be simpler/faster with monospaced fonts | |
| # todo: | |
| # line-wrapping? |