简体   繁体   English

Dash 在 PyQt QThread() 中运行 - 使用新数据重新初始化的最合适方式

[英]Dash running in a PyQt QThread() - most appropriate way to re-initialize with new data

At the moment I have a PyQt5 app running Dash in a QThread like so:目前我有一个在 QThread 中运行 Dash 的 PyQt5 应用程序,如下所示:

class MapView(QObject):

    message = pyqtSignal(str)
    shutting_down = pyqtSignal(bool)

    def __init__(self, data):
        super().__init__()
        self.app = dash.Dash(__name__)
        self.data = data
    
        #manual callbacks
        self.app.callback(
            Output('hover-data', 'children'),
            Input('basic-interactions', 'hoverData'))(self.display_hover_data)

        self.app.callback(
            Output('page-content', 'children'),
            Input('url', 'pathname'))(self.shutdown)
        
#...more callbacks

    def shutdown(self, pathname):
        if pathname != '/shutdown':
            return
        print("Trying to shutdown map dash")
        func = request.environ.get('werkzeug.server.shutdown')
        if func is None:
            raise RuntimeError('Not running with the Werkzeug Server')
        func()
        self.shutting_down.emit(True)

#.....lots of stuff like graphs, the layout, a function to start the server etc

and then the threading code in my main pyqt5 app is:然后我的主要 pyqt5 应用程序中的线程代码是:

def show_map(self):
        
        self.mapthread = QThread()
        self.map = map_dash.MapView(self.data) 
        self.map.moveToThread(self.mapthread)
        self.map.message.connect(self.update_output)
        self.map.shutting_down.connect(self.close_map_thread)
        self.mapthread.finished.connect(self.open_project)
        self.mapthread.started.connect(self.map.run)
        self.mapthread.start()
        self.browser.setUrl(QUrl('http://127.0.0.1:8050'))
        self.update_output("Map plotted.")

My initial problem here was that if I tried to run show_map() with new data when Dash was already running (the user goes to File -> Open when a project is already running) , I would get the fatal Qthread: destroyed while thread is still running .我最初的问题是,如果我在 Dash 已经运行时尝试使用新数据运行show_map() (当项目已经运行时,用户转到File -> Open ),我会得到致命的Qthread: destroyed while thread is still running So I embarked along the path of shutting down the flask server, then closing the thread, then going back to the function that opens a new project.于是我走上了关闭flask服务器,然后关闭线程,再回到打开新项目的函数的路径。 It's like this:就像这样:

  1. User goes to File -> Open用户转到File -> Open
  2. def open_project() checks mapthread.isRunning() def open_project()检查mapthread.isRunning()
  3. It's not running so it opens a new project, creates a new QThread and new MapView instance它没有运行所以它打开一个新项目,创建一个新的QThread和新的MapView实例
  4. User goes to File -> Open again用户转到File -> Open再次File -> Open
  5. The check in (2) returns True so the flask server is asked to shut down签入 (2) 返回 True,因此要求关闭 Flask 服务器
  6. After the server has shut down, the shutting_down signal causes the thread to be asked to quit() (I'm not sure how robust this is because it isn't a signal from flask, just a line after I've asked it to shut down. But it seems to work for now).服务器关闭后,shutting_down 信号导致线程被要求quit() (我不确定这有多健壮,因为它不是来自烧瓶的信号,只是在我要求它之后的一行关闭。但它现在似乎工作)。
  7. Once the thread has finished, the thread emits finished() which calls open_project() again线程完成后,线程发出finished() open_project()再次调用open_project()
  8. open_project this time sees that the thread is not running and allows the user to open a new file. open_project这次看到线程没有运行,允许用户打开一个新文件。

4 to 8 doesn't take too long to run but it all seems a bit complicated and the Dash layout glitches a bit for whatever reason. 4 到 8 的运行时间不会太长,但这一切似乎有点复杂,而且无论出于何种原因,Dash 布局都会出现一些小故障。 As it stands, when the QThread finishes under any other circumstances it would call open_project (although I could probably work around that).就目前而言,当 QThread 在任何其他情况下完成时,它会调用open_project (尽管我可能会解决这个问题)。 Is there a better way to do this?有一个更好的方法吗? Can I feed the existing map instance new data somehow?我可以以某种方式为现有地图实例提供新数据吗? I have read the documentation on dcc.Interval but that doesn't seem a great way to do it either...我已经阅读了关于dcc.Interval的文档,但这似乎也不是一个很好的方法......

Update (as per comments below):更新(根据下面的评论):

Now what I'm doing is passing new data to the thread self.map.data = new_data , then using a callback on url to re-draw the map and refresh the layout.现在我正在做的是将新数据传递给线程self.map.data = new_data ,然后使用 url 上的回调重新绘制地图并刷新布​​局。 Exactly like this:就像这样:

    elif pathname == '/refresh':
            self.draw_map()
            self.set_layout()

But here is the problem.但问题就在这里。 self.set_layout() refreshes dash with the OLD figure. self.set_layout()使用 OLD 图形刷新破折号。 I have verified that self.draw_map() is drawing a new figure.我已经验证self.draw_map()正在绘制一个新图形。 But calling /refresh a second time does cause dash to use the new figure.但是第二次调用 /refresh 确实会导致 dash 使用新数字。 So is dash storing the old figure in a cache that hasn't been updated in the fraction of a second it takes to set the layout again?那么 dash 是否将旧图形存储在一个缓存中,并且在几分之一秒内还没有更新,以便再次设置布局? How can I make dash wait for the cache to be updated?如何让 dash 等待缓存更新?

One way to do this is to pass the new data into the dash instance一种方法是将新数据传递到 dash 实例

self.map.data = new_data

but this does not refresh the layout and you can't simply call但这不会刷新布局,您不能简单地调用

self.map.set_layout() from the main thread. self.map.set_layout()来自主线程。 Instead you have to use the url callback like this:相反,您必须像这样使用 url 回调:

def shutdown(self, pathname):
        if pathname == '/shutdown':
            print("Trying to shutdown map dash")
            func = request.environ.get('werkzeug.server.shutdown')
            if func is None:
                raise RuntimeError('Not running with the Werkzeug Server')
            func()
            self.shutting_down.emit(True)
        elif pathname == '/refresh':
            self.draw_map()
            self.set_layout()

of course it would be more appropriate to call that method something like handle_url() now.当然,现在将该方法称为像handle_url()这样的方法会更合适。

But there is a further problem.但还有一个问题。 I'm guessing due to some caching issue, dash displays the old version of the figure unless the user manually calls refresh again.我猜是由于某些缓存问题,除非用户再次手动调用刷新,否则 dash 会显示该图的旧版本。 The solution is to add a dcc.Interval callback that updates the figure (it calls draw_map() ).解决方案是添加一个dcc.Interval回调来更新图形(它调用draw_map() )。 It doesn't need to be doing this every second, you can set the interval to 999999, for example.不需要每秒都执行此操作,例如,您可以将间隔设置为 999999。 In fact you can set 'max_intervals=1' and it still works.事实上,您可以设置 'max_intervals=1' 并且它仍然有效。 As long as the callback is there, it will be called the first time the page refreshes and the figure will be updated.只要有回调,就会在页面第一次刷新时调用,并更新图形。

In fact, with that in place, the draw_map() and set_layout() functions don't even need to be called in the url callback.事实上,有了这个, draw_map()set_layout()函数甚至不需要在 url 回调中调用。 So the code now looks like this:所以代码现在看起来像这样:

def shutdown(self, pathname):
        if pathname == '/shutdown':
            print("Trying to shutdown map dash")
            func = request.environ.get('werkzeug.server.shutdown')
            if func is None:
                raise RuntimeError('Not running with the Werkzeug Server')
            func()
            self.shutting_down.emit(True)
        elif pathname == '/refresh':
            print("I'm doing nothing")

And it works.它有效。 But the url callback itself IS necessary.但是 url 回调本身是必要的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM