【问题标题】:wxpython StaticText.SetLabel doesn't update in a single-threaded GUI when .Wrap is used?wxpython StaticText.SetLabel 在使用 .Wrap 时不会在单线程 GUI 中更新?
【发布时间】:2016-10-19 14:13:11
【问题描述】:

我有一个长期运行的 Python 脚本,目前是终端驱动的。我需要在进程中放置一个 GUI 前端,以使其更加用户友好。

目前,GUI 和长时间运行的进程都在同一个线程中运行。 (我计划在未来通过为 long 函数生成一个新线程来改进这一点)。我为那个长时间运行的线程提供了一些回调,以便它可以偶尔更新 GUI 以帮助衡量其进度。

由于某种原因,回调能够直观地更新位图,但不能更新标签,即使在这两种情况下都调用了 .Refresh() 和 .Update()。

我注意到,如果我在 on_user_action_label_wrap 中删除对 wrap 的调用,那么它确实会更新标签和位图。

问题

a) 为什么使用.Wrap会影响标签的更新?
b) **是否可以强制更新/重绘 `on_user_action_label_wrap 中的标签?或者真的,只是将文本包装并更新? **

(“使用多个线程”不是一个可接受的答案:)这最终会发生,但现在我想了解为什么这不起作用)

以下是 SSCCE。我已经在 python 2.7、wxpython 3.0.2.0、wx-3.0-gtk2、gtk+ 2.24.30-1ubuntu1 和 2.24.25-3+deb8u1 以及 Ubuntu 16.04 和 Raspbian。同样的问题。

#!/usr/bin/env python
# coding=utf-8
from __future__ import absolute_import, division, print_function

import collections
import wx
from time import sleep

STATE_START = "Start!"
STATE_MIDDLE = "Middle!"
STATE_END = "End!"
STATE_STOP = "Stop!"

STATES = [STATE_START, STATE_MIDDLE, STATE_END, STATE_STOP]


class SingleThreadedKeyLoader(wx.Dialog):
    ICON_SIZE = (32, 32)

    def __init__(self, parent):
        wx.Dialog.__init__(self, parent, title="Do stuff",
                           size=wx.Size(800, 600),
                           style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.STAY_ON_TOP)
        self.SetSizeHintsSz(wx.DefaultSize, wx.DefaultSize)

        self.active_state_bmp = wx.ArtProvider.GetBitmap(wx.ART_GO_FORWARD,
                                                         wx.ART_CMN_DIALOG,
                                                         self.ICON_SIZE)
        self.done_state_bmp = wx.ArtProvider.GetBitmap(wx.ART_TICK_MARK,
                                                       wx.ART_CMN_DIALOG,
                                                       self.ICON_SIZE)
        if wx.NullBitmap in [self.active_state_bmp, self.done_state_bmp]:
            raise Exception("Failed to load icons")

        self._make_ui()

        self.current_state = STATE_START

    def _make_ui(self):

        dialog_sizer = wx.BoxSizer(wx.VERTICAL)

        top_half_sizer = wx.BoxSizer(wx.HORIZONTAL)

        #########
        # User action label
        label_sizer = wx.BoxSizer(wx.VERTICAL)

        self.panel = wx.Panel(self, wx.ID_ANY)
        self.user_action_label = wx.StaticText(self.panel,
                                               label="",
                                               style=wx.ALIGN_CENTRE)
        self.user_action_label.Wrap(100)  # DOESN't SEEM TO  WORK <---------
        self.user_action_label.Bind(wx.EVT_SIZE,
                                    self.on_user_action_label_wrap)
        label_sizer.Add(self.user_action_label, 1,
                        wx.EXPAND | wx.ALIGN_CENTER | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL,
                        5)

        self.panel.SetSizer(label_sizer)
        top_half_sizer.Add(self.panel, 1, wx.EXPAND, 5)
        #
        #########

        #########
        # combo box + log area. Just here to get in the way of long text
        log_area_sizer = wx.BoxSizer(wx.VERTICAL)

        self.log_textbox = wx.TextCtrl(self, value=wx.EmptyString,
                                       style=wx.TE_MULTILINE | wx.TE_READONLY)
        log_area_sizer.Add(self.log_textbox, 1,
                           wx.ALIGN_TOP | wx.ALL | wx.EXPAND, 5)

        self.log_combobox_choices = ["Debug", "Info", "Warning"]
        self.log_combobox = wx.ComboBox(self, value="Info",
                                        choices=self.log_combobox_choices)
        log_area_sizer.Add(self.log_combobox, 0,
                           wx.ALIGN_BOTTOM | wx.ALL | wx.EXPAND, 5)

        top_half_sizer.Add(log_area_sizer, 2, wx.EXPAND, 5)

        dialog_sizer.Add(top_half_sizer, 2, wx.EXPAND, 5)
        #
        #########

        #########
        # State tracking bitmaps. The image should be updated as long function
        # runs.
        state_tracker_sizer = wx.WrapSizer(wx.HORIZONTAL)
        self.bitmaps_od = self.generate_state_tracker(self,
                                                      state_tracker_sizer)
        dialog_sizer.Add(state_tracker_sizer, 1, wx.EXPAND, 5)
        #
        #########

        #########
        # Buttons! A, B, C are here just to check the text-wrap is working
        # in the box.
        button_sizer = wx.BoxSizer(wx.HORIZONTAL)

        self.button_a = wx.Button(self, label=u"A")
        self.button_a.Bind(wx.EVT_BUTTON, self.on_clicked_a)
        button_sizer.Add(self.button_a, 0, wx.ALL, 5)

        self.button_b = wx.Button(self, label=u"B")
        self.button_b.Bind(wx.EVT_BUTTON, self.on_clicked_b)
        button_sizer.Add(self.button_b, 0, wx.ALL, 5)

        self.button_c = wx.Button(self, label=u"C")
        self.button_c.Bind(wx.EVT_BUTTON, self.on_clicked_c)
        button_sizer.Add(self.button_c, 0, wx.ALL, 5)

        self.button_long_func = wx.Button(self, label=u"Long running func")
        self.button_long_func.Bind(wx.EVT_BUTTON, self.on_clicked_long_func)
        button_sizer.Add(self.button_long_func, 0, wx.ALL, 5)

        dialog_sizer.Add(button_sizer, 0, wx.EXPAND, 5)
        #
        #########

        self.SetSizer(dialog_sizer)
        self.Layout()

        self.Centre(wx.BOTH)

    @staticmethod
    def generate_state_tracker(parent, state_tracker_sizer):
        """
        Generates the staticbitmap and statictext.
        The bitmaps will change from arrows to ticks during the process
        """
        def state_label(i, state):
            return "{:>02}. {}".format(i + 1, str(state))

        f = parent.GetFont()
        dc = wx.WindowDC(parent)
        dc.SetFont(f)

        label_bitmaps = collections.OrderedDict()
        max_string_width = -1
        for i, state in enumerate(STATES):
            width, height = dc.GetTextExtent(state_label(i, state))
            max_string_width = max(max_string_width, width)

        for i, state in enumerate(STATES):
            state_sizer = wx.BoxSizer(wx.HORIZONTAL)

            bitmap_name = "state{}_{}".format(i, "bitmap")
            bitmap = wx.StaticBitmap(parent, bitmap=wx.NullBitmap,
                                     name=bitmap_name,
                                     size=SingleThreadedKeyLoader.ICON_SIZE)
            state_sizer.Add(bitmap, 0, wx.ALL, 5)
            label_bitmaps[bitmap_name] = bitmap
            label_bitmaps[state] = bitmap

            label_sizer = wx.BoxSizer(wx.HORIZONTAL)
            label = wx.StaticText(parent, label=state_label(i, state),
                                  size=wx.Size(max_string_width, -1))
            label_sizer.Add(label, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 0)
            state_sizer.Add(label_sizer, 1, wx.EXPAND, 5)

            state_tracker_sizer.Add(state_sizer, 1, wx.EXPAND, 5)

        return label_bitmaps

    def on_clicked_a(self, event):
        """ wrapping test: Sets the label to a small amount of text"""
        self.user_action_label.SetLabel("A this is a small label")

    def on_clicked_b(self, event):
        """ wrapping test: Sets the label to a medium amount of text"""
        self.user_action_label.SetLabel(
            "B B B This is a medium label. It is longer than a small label but shorter than the longer label")

    def on_clicked_c(self, event):
        """ wrapping test: Sets the label to a large amount of text"""
        self.user_action_label.SetLabel(
            "C C C C C C C This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. ")

    def my_set_state_callback(self, new_state):
        """
        Updates the lovely tickboxes.

        Used as a callback from the long running function.
        """
        if new_state == self.current_state:
            return

        print(self.current_state, "->", new_state)

        self.bitmaps_od[self.current_state].SetBitmap(self.done_state_bmp)
        self.bitmaps_od[new_state].SetBitmap(self.active_state_bmp)
        self.current_state = new_state

        self.Refresh()
        self.Update()

    def my_user_feedback_callback(self, message):
        """
        Updates the label that insturcts the user to do stuff

        Used as a callback from the long running function.
        """
        print("my_user_feedback_fn", message)
        self.user_action_label.SetLabel(message)

        self.Refresh()
        self.Update()

    def on_clicked_long_func(self, event):
        """ launches the long function on button press """

        self.button_a.Enable(False)
        self.button_b.Enable(False)
        self.button_c.Enable(False)
        self.log_combobox.Enable(False)
        self.button_long_func.Enable(False)

        self.bitmaps_od[STATE_START].SetBitmap(self.active_state_bmp)
        wx.CallAfter(my_long_running_func, self.my_set_state_callback,
                     self.my_user_feedback_callback)



    def on_user_action_label_wrap(self, event):
        """ Wraps the long text in the box on resize. """
        self.user_action_label.Wrap(self.panel.Size[0])    #<------------------------ problem?
        event.Skip() # ?


def my_long_running_func(set_state, user_feedback_fn):
    """
    This is a very long runing function that uses callbacks to update the
    main UI.
    """
    user_feedback_fn(
        "This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. This is a very long string. ")
    set_state(STATE_START)
    sleep(2)

    set_state(STATE_MIDDLE)
    sleep(2)
    user_feedback_fn("do the thing")

    # If this was the real thing then here we'd pause until the user obeyed
    # the instruction or something
    # wait_for_user_to_do_the_thing()

    set_state(STATE_END)
    sleep(0.5)

    user_feedback_fn("DONE")
    set_state(STATE_STOP)
    print("returning")
    return


app = wx.App(False)

x = SingleThreadedKeyLoader(None)
x.ShowModal()
x.Destroy()

app.MainLoop()

【问题讨论】:

  • 你的 GTK+ 版本是什么?
  • @Igor 更新了两个 gtk+ 版本。
  • C++ 版本有 wxYieldIfNecessary()。我不确定 wxPython 对应的是什么,但应该不难找到。试试看。

标签: python wxpython wxwidgets single-threaded


【解决方案1】:

您正在为对话框调用RefreshUpdate,但对话框不是正在更改的内容,而是标签。在某些情况下,对话框的Update 也会导致子小部件也被重新绘制,但并非总是如此。但是,在此示例中需要处理的事件不仅仅是绘制新标签,因此您需要让事件循环定期运行,以便也可以处理其他事件。当您这样做时,标签的刷新也将得到处理。因此,在您的示例中,将对 RefreshUpdate 的调用替换为对 wx.Yield() 的调用,它应该会更好地工作。

【讨论】:

  • 似乎有效。谢谢! wx.Yield() 应该完全替换刷新调用,还是在子小部件上仍然需要它们? ps:我怀疑它不起作用的原因是他们需要更多需要泵送的消息,但无法弄清楚处理它的事件需要哪个窗口/小部件。有什么线索吗?
猜你喜欢
  • 1970-01-01
  • 2014-05-24
  • 2014-04-16
  • 2014-04-06
  • 2012-12-12
  • 2023-04-04
  • 1970-01-01
  • 2019-05-03
  • 1970-01-01
相关资源
最近更新 更多