在 Python 中使用 Dash 库编写前端页面有时需要引入 JavaScript 代码实现纯 Dash 库无法实现的特殊功能,比如监听浏览器页面中鼠标的位置。
直接使用 html.Script 能实现加载 JS 文件,但无法直接通过它执行 JavaScript 代码;额外引入 dash-extensions 库可以使用其封装好的事件监听函数 EventListener 监听鼠标位置,但是该库资料较少,容易错误使用,bug 排查较为困难。
Dash 官方文档中推荐以 clientside_callback 客户端回调的方式引入 Javascript 代码使用,因此本文采取此方式引入 Javascript 代码实现带关闭按钮的悬浮框拖拽功能,JS 代码与 Python 代码分离便于管理维护。
script.js
/*--------窗口拖动功能--------*/
// 点击窗口触发拖拽
window.dash_clientside = Object.assign({}, window.dash_clientside, {
clientside: {
container_drag_function:
function (n_clicks, currentPos) {
//const handle = document.getElementById('drag-handle'); // 只拖动头部
const dialog = document.getElementById('draggable-dialog');
// 可视窗口的大小
let winWid = document.documentElement.clientWidth || document.body.clientWidth;
let winHei = document.documentElement.clientHeight || document.body.clientHeight;
// 可视窗口大小减掉所需移动的窗口大小,得出这个窗口所能达到的最边缘值,中间值为0时完全贴合窗口边缘
winWid = winWid - 0 - dialog.offsetWidth
winHei = winHei - 0 - dialog.offsetHeight
if (!window.dragSetup) {
window.dragSetup = true;
let isDragging = false;
let offsetX, offsetY;
dialog.addEventListener('mousedown', function (e) {
isDragging = true;
offsetX = e.clientX - dialog.offsetLeft;
offsetY = e.clientY - dialog.offsetTop;
e.preventDefault();
});
document.addEventListener('mousemove', function (e) {
if (isDragging) {
// 不要用const,会让拖拽变卡,并且下面if判断中的newX的值会无法更新
let newX = e.clientX - offsetX;
let newY = e.clientY - offsetY;
if (newX <= 0) {
newX = 1
}
if (newY <= 0) {
newY = 1
}
if (newX > winWid) {
newX = winWid
}
if (newY > winHei) {
newY = winHei
}
dialog.style.left = newX + 'px';
dialog.style.top = newY + 'px';
return {'x': newX, 'y': newY};
}
return dash_clientside.no_update;
});
document.addEventListener('mouseup', function () {
isDragging = false;
});
}
return currentPos;
}
}
});
/*--------窗口拖动功能--------*/
dialog.py
import dash
from dash import html, Input, Output, callback, dcc, ClientsideFunction
app = dash.Dash(__name__)
app.layout = html.Div([
html.Div(
id="draggable-dialog",
children=[
html.Div(
children=[
html.Span("可拖动对话框", style={'flex': 1}),
html.Button('×', id='close-btn', n_clicks=0,
style={'background': 'none', 'border': 'none',
'font-size': '20px', 'cursor': 'pointer', 'float': 'right'})
],
id="drag-handle",
style={'padding': '10px', 'background': '#4285f4',
'color': 'white', 'cursor': 'move'}),
html.Div("对话框内容区域",
style={'padding': '20px', 'border': '1px solid #ddd'})
],
style={
'position': 'fixed',
'width': '300px',
'border-radius': '5px',
'box-shadow': '0 4px 8px rgba(0,0,0,0.1)',
'z-index': 1000
}
),
# 此处设置的x、y初始值对容器位置没有影响
dcc.Store(id='position-store', data={'x': 0, 'y': 0})
])
# 按钮对话框关闭功能
@callback(
Output('draggable-dialog', 'style', allow_duplicate=True),
Input('close-btn', 'n_clicks'),
prevent_initial_call=True
)
def close_dialog(n_clicks):
if n_clicks > 0:
return {'display': 'none'}
return dash.no_update
# 通过在定义的clientside_callback客户端回调函数中引入鼠标位置监听的javascript代码
# 容器拖拽功能的回调直接在浏览器中运行,以减少服务器端开销成本
app.clientside_callback(
# 调用assets文件夹中的js代码
ClientsideFunction(
namespace='clientside',
function_name='container_drag_function'
),
Output('position-store', 'data'),
Input('draggable-dialog', 'n_clicks'),
Input('position-store', 'data'),
prevent_initial_call=True
)
if __name__ == "__main__":
app.run_server(debug=True)
注:script.js 文件放置在assets文件夹中,dialog.py 文件与assets文件夹在同一目录下。