活动介绍

怎么设置QDockWidget 即在左侧又在底部

时间: 2025-07-19 16:30:56 浏览: 23
<think>我们有一个主窗口,左侧有一个QDockWidget(lDock),底部有一个QDockWidget(btmDock)。现在要添加一个新的QDockWidget(newDock),要求: 1. 位于lDock的下方,btmDock的左侧。 2. 高度与btmDock一样,宽度与lDock一样。 在Qt中,一个QDockWidget只能有一个停靠区域(比如左侧、右侧、顶部、底部),但是我们可以通过分割区域来实现同时位于左侧区域和底部区域的效果。 思路: 1. 首先,将newDock垂直分割到lDock的下方(这样newDock就位于左侧区域,在lDock下面)。 2. 然后,将newDock水平分割到底部区域(btmDock的左侧)。但是,由于一个DockWidget不能同时位于两个区域,所以我们需要通过分割底部区域来实现。 具体步骤: a. 将newDock添加到左侧区域,并分割在lDock下方。 b. 将底部区域(btmDock所在区域)水平分割,将newDock添加到btmDock的左侧。 但是,直接这样做可能会遇到问题,因为newDock已经属于左侧区域,我们需要将其移动到新的位置。实际上,我们可以通过重新分割来实现。 另一种方法是使用占位符,但更直接的方法是使用Qt的splitDockWidget和tabifyDockWidget函数来精确控制布局。 参考引用[3]中提到的停靠区域,我们可以通过多次分割来实现。 步骤详解: 1. 首先,将lDock和btmDock添加到主窗口: mainWindow->addDockWidget(Qt::LeftDockWidgetArea, lDock); mainWindow->addDockWidget(Qt::BottomDockWidgetArea, btmDock); 2. 然后,添加newDock,先将其放在左侧区域,并分割在lDock下方: mainWindow->splitDockWidget(lDock, newDock, Qt::Vertical); 此时,newDock在lDock的下方,但占据了整个左侧区域的下面部分,而btmDock占据整个底部区域。 3. 现在,我们希望newDock也出现在底部区域的左侧,同时btmDock在底部区域的右侧。我们可以将底部区域分割成左右两部分,左边放newDock,右边放btmDock。但是,由于newDock已经在左侧区域,我们需要将其移动到新的位置。 注意:一个DockWidget不能同时属于两个区域,但我们可以通过重新排列来实现。 4. 我们可以将底部区域看作一个水平分割的容器,将newDock和btmDock并排放置。但是,由于newDock已经在左侧区域,我们需要先将其从左侧区域移除,然后添加到新的分割中。 然而,Qt提供了另一种方式:我们可以直接分割底部区域,但是需要先有一个占位的dock,然后替换掉。 5. 更简单的方法:使用两次分割操作,先将newDock放在左侧区域(lDock下方),然后再将newDock添加到底部区域(与btmDock水平分割)。但是,Qt允许一个dock在多个区域吗?实际上,一个dock只能在一个区域,但我们可以通过分割来实现同时连接两个区域的效果。 6. 实际上,我们可以这样操作: - 首先,将newDock垂直分割到lDock下方(这样newDock位于左侧区域的下部)。 - 然后,将newDock和btmDock进行水平分割,但是这样会改变底部区域的布局。 然而,这样操作后,newDock实际上同时连接了左侧区域和底部区域,形成类似“L”形的布局。 7. 具体代码: // 添加左侧和底部dock mainWindow->addDockWidget(Qt::LeftDockWidgetArea, lDock); mainWindow->addDockWidget(Qt::BottomDockWidgetArea, btmDock); // 将newDock分割到lDock下方(垂直分割) mainWindow->splitDockWidget(lDock, newDock, Qt::Vertical); // 现在,将底部区域分割:我们希望将newDock也放入底部区域,并放在btmDock的左边 // 注意:此时btmDock占据整个底部区域,我们需要将底部区域水平分割成两部分:左边放newDock,右边放btmDock // 但是,splitDockWidget的第一个参数是目标dock,第二个参数是新的dock,方向是水平方向 // 由于btmDock已经在底部区域,我们可以将newDock分割到btmDock的左边 mainWindow->splitDockWidget(btmDock, newDock, Qt::Horizontal); // 但是,这样操作后,newDock会出现在btmDock的左边,同时它也会从左侧区域移除吗?不会,它同时出现在两个区域?实际上,这样操作后,newDock会从原来的位置(左侧区域下方)移动到新的位置(底部区域)?我们需要测试。 8. 根据Qt文档,一个DockWidget只能存在于一个区域。当我们执行第二次splitDockWidget时,实际上是将newDock从原来的分割中移除,然后与btmDock进行水平分割。这样,newDock就会移动到btmDock所在的区域(底部区域),并位于btmDock的左侧。但是,这样我们就失去了在左侧区域下方的位置。 9. 我们想要的效果是:newDock既在lDock的下方(垂直方向),又在btmDock的左侧(水平方向),也就是说,它应该占据主窗口左下角的一个矩形区域,同时与lDock和btmDock相邻。 10. 实际上,我们可以通过将左侧区域和底部区域进行分割组合来实现。具体做法是: a. 首先,将左侧区域垂直分割:lDock在上,newDock在下。 b. 然后,将底部区域水平分割:newDock在左,btmDock在右。 但是,这样newDock就同时属于左侧区域和底部区域?在Qt中,这是通过分割器连接两个区域来实现的,实际上newDock属于一个区域(可能是左侧区域),而底部区域被分割成两个部分:左侧部分(newDock)和右侧部分(btmDock)。但是,newDock同时出现在左侧区域的下方和底部区域的左侧,这是Qt布局管理器的特性。 11. 我们尝试以下代码: // 添加左侧dock mainWindow->addDockWidget(Qt::LeftDockWidgetArea, lDock); // 添加底部dock mainWindow->addDockWidget(Qt::BottomDockWidgetArea, btmDock); // 在左侧区域,将lDock和newDock垂直分割 mainWindow->splitDockWidget(lDock, newDock, Qt::Vertical); // 现在,将newDock和btmDock进行水平分割,注意:此时btmDock在底部区域,而newDock在左侧区域,我们想要将newDock也拉到底部区域,并放在btmDock的左边 // 但是,如果我们直接分割btmDock和newDock,那么newDock会被移动到btmDock所在的区域(底部区域),并与btmDock水平排列,这样newDock就不再位于lDock下方了。 12. 我们需要另一种方式:在底部区域创建一个水平分割,将newDock和btmDock放在一起,然后将这个水平分割的区域作为整个底部区域。同时,保持左侧区域的垂直分割(lDock和newDock)不变。但是,这样newDock就同时属于两个区域?实际上,在Qt中,同一个DockWidget不能同时属于两个区域,所以我们需要重新思考。 13. 实际上,我们可以将主窗口的左侧区域和底部区域进行组合。具体做法是: a. 首先,将左侧区域垂直分割成两部分:lDock和newDock(lDock在上,newDock在下)。 b. 然后,将整个左侧区域(包括lDock和newDock)和底部区域进行分割?不对。 14. 正确的方法是使用嵌套分割: a. 首先,将主窗口的左侧区域设置为lDock。 b. 然后,在左侧区域下方分割出newDock(垂直分割),这样左侧区域现在包含两个dock:lDock和newDock。 c. 接着,将底部区域分割成两部分:左边是newDock,右边是btmDock。但是,newDock已经在左侧区域,我们不能直接将其放入底部区域。 15. 其实,我们可以通过将底部区域分割成两个部分,然后将左侧区域(包括两个dock)和底部区域进行组合。但是,这需要理解Qt的布局机制。 16. 另一种思路:使用Qt的API `addDockWidget` 和 `setDockOptions` 可能不够,我们需要使用 `createDockWidget` 和 `setCorner` 来设置角落的归属。 17. 实际上,我们可以这样: // 添加左侧dock:lDock mainWindow->addDockWidget(Qt::LeftDockWidgetArea, lDock); // 添加底部dock:btmDock mainWindow->addDockWidget(Qt::BottomDockWidgetArea, btmDock); // 现在,将newDock添加到左侧区域,并放在lDock的下方(垂直分割) mainWindow->splitDockWidget(lDock, newDock, Qt::Vertical); // 然后,将底部区域分割:我们希望将底部区域分成左右两个部分,左边部分放newDock,右边部分放btmDock // 但是,newDock已经在左侧区域,我们不能直接将其放入底部区域。所以,我们需要先将newDock从左侧区域移除,然后重新添加到新的位置。 // 移除newDock(隐藏并重新添加) newDock->setFloating(true); // 让其浮动 // 然后将其停靠到我们想要的位置 // 但是,这样会破坏布局。 18. 参考引用[4]中提到:QDockWidget的尺寸策略应该由其内部的widget来实现,我们不应该直接设置QDockWidget的尺寸限制。但是,我们可以通过设置内部widget的尺寸策略来影响dock的尺寸。 19. 实际上,我们可以通过两次分割操作来实现,但需要按照正确的顺序: a. 首先,添加lDock到左侧区域。 b. 添加btmDock到底部区域。 c. 然后,添加newDock到底部区域,并将其分割在btmDock的左侧(水平分割),这样底部区域就分为newDock和btmDock。 d. 然后,将newDock从底部区域移动到左侧区域,并放置在lDock的下方(垂直分割)。这样,newDock就会同时出现在两个区域?不会,移动后它就会离开底部区域。 20. 正确的方法是:先构建左侧区域(包括lDock和newDock的垂直分割),然后再构建底部区域(包括newDock和btmDock的水平分割)。但是,同一个newDock不能同时出现在两个分割中。 21. 因此,我们需要两个不同的dock区域,但newDock只能属于一个。所以,我们只能让newDock属于左侧区域,然后在底部区域放置另一个dock?这不符合要求。 22. 实际上,我们想要的效果可以通过将主窗口的角落设置为左侧区域和底部区域重叠的部分来实现。在Qt中,我们可以通过`setCorner`函数来设置哪个区域占据角落。 例如,默认情况下,左下角是由左侧区域和底部区域共享的。我们可以设置左下角属于左侧区域,这样左侧区域就会延伸到整个左边(包括底部区域的上方和下方),而底部区域只占据右侧部分。 但是,这样设置后,我们添加dock的顺序会影响布局。 23. 步骤: a. 设置左下角为左侧区域: mainWindow->setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); b. 添加lDock到左侧区域(此时lDock会占据左侧区域的顶部)。 c. 添加newDock到左侧区域(此时newDock会占据左侧区域的底部,也就是主窗口的左下角)。 d. 添加btmDock到底部区域(此时底部区域在左下角被左侧区域占据,所以btmDock只能占据底部区域的右侧部分)。 这样,newDock就会位于lDock的下方,同时位于btmDock的左侧,而且高度与btmDock一样(因为都在底部,但实际高度可能由分割器决定)?宽度与lDock一样(因为同属左侧区域)? 24. 但是,newDock的高度不一定等于btmDock的高度,因为newDock在左侧区域,而btmDock在底部区域,它们的高度由各自的分割器控制。 25. 我们需要在布局完成后,手动设置newDock的高度等于btmDock的高度,宽度等于lDock的宽度,并且当btmDock高度变化时,newDock的高度也变化,当lDock宽度变化时,newDock的宽度也变化。 26. 因此,我们这样做: a. 设置左下角为左侧区域: mainWindow->setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); b. 添加lDock到左侧区域: mainWindow->addDockWidget(Qt::LeftDockWidgetArea, lDock); c. 添加newDock到左侧区域: mainWindow->addDockWidget(Qt::LeftDockWidgetArea, newDock); 此时,newDock会出现在lDock的下方(因为左侧区域是垂直排列的)。 d. 添加btmDock到底部区域: mainWindow->addDockWidget(Qt::BottomDockWidgetArea, btmDock); 此时,btmDock会占据底部区域,而底部区域在左下角已经被左侧区域占据,所以btmDock只能占据底部区域的右侧部分,左侧部分就是newDock(因为newDock在左侧区域的底部,正好在左下角)。 27. 这样,布局就形成了: +-------------------+-----------------------+ | | | | lDock | | | | | +-------------------+ | | | | | newDock | central | | | | +---------+---------+-----------------------+ | | | | | newDock | btmDock | | | | | | +---------+---------+-----------------------+ 注意:newDock实际上是一个,它同时出现在两个位置?不,它只是被拉长了,它属于左侧区域,所以占据整个左侧区域的底部,而底部区域中的左侧部分就是newDock的延伸。 28. 但是,这样设置后,newDock的宽度和高度会自适应。我们需要设置newDock的高度与btmDock的高度相同,宽度与lDock相同。 由于布局是动态的,我们可以监听尺寸变化事件,或者使用定时器在布局完成后设置尺寸。 29. 设置尺寸的代码(在布局完成后): // 设置newDock的宽度等于lDock的宽度 newDock->setFixedWidth(lDock->width()); // 设置newDock的高度等于btmDock的高度 newDock->setFixedHeight(btmDock->height()); 但是,这样设置后,当用户调整lDock或btmDock的大小时,newDock的尺寸不会自动调整。因此,我们需要连接信号: connect(lDock, &QDockWidget::resized, [=](){ newDock->setFixedWidth(lDock->width()); }); connect(btmDock, &QDockWidget::resized, [=](){ newDock->setFixedHeight(btmDock->height()); }); 但是,QDockWidget没有resized信号,我们可以通过重写resizeEvent或者使用事件过滤器来实现。或者,我们可以监听主窗口的分割器移动事件。 30. 另一种方法是使用QTimer::singleShot在事件循环后设置一次,然后每次布局变化时都调整。但这样效率较低。 31. 我们可以使用事件过滤器来监听lDock和btmDock的resize事件。 32. 但是,更简单的方式是:在每次布局变化时(比如主窗口的resizeEvent),我们调整newDock的尺寸。但这样可能过于频繁。 33. 实际上,我们可以将newDock内部的widget设置为固定尺寸,然后根据lDock和btmDock的尺寸调整这个widget的尺寸,并设置newDock的最小尺寸和最大尺寸为这个固定尺寸,这样分割器就不会改变它的尺寸。但是,这样用户就不能通过拖动分割器来调整newDock的尺寸了,只能通过调整lDock和btmDock来改变。 34. 根据需求,我们就是要newDock的宽度等于lDock的宽度,高度等于btmDock的高度,所以固定尺寸是符合要求的。 35. 因此,我们可以在每次lDock或btmDock改变大小时,调整newDock的固定尺寸。 36. 实现: // 在newDock内部设置一个widget,并设置这个widget的尺寸策略为Fixed,然后通过调整widget的尺寸来调整dock的尺寸。 QWidget *innerWidget = new QWidget(); newDock->setWidget(innerWidget); // 然后,我们调整innerWidget的固定尺寸,但这样还不够,因为dock的尺寸还包括标题栏等。 37. 实际上,我们可以直接设置newDock的固定尺寸,但是要注意,当dock被拖动浮动时,固定尺寸可能会出现问题。所以,我们需要在dock停靠时才设置固定尺寸。 38. 我们可以监听newDock的topLevelChanged信号,当不是浮动状态时,设置固定尺寸。 39. 综上所述,代码步骤: // 设置左下角为左侧区域 mainWindow->setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); // 添加lDock到左侧区域 mainWindow->addDockWidget(Qt::LeftDockWidgetArea, lDock); // 添加newDock到左侧区域 mainWindow->addDockWidget(Qt::LeftDockWidgetArea, newDock); // 添加btmDock到底部区域 mainWindow->addDockWidget(Qt::BottomDockWidgetArea, btmDock); // 在布局完成后,设置newDock的固定宽度和高度 QTimer::singleShot(0, [=]() { newDock->setFixedWidth(lDock->width()); newDock->setFixedHeight(btmDock->height()); }); // 监听lDock和btmDock的尺寸变化 // 由于QDockWidget没有resized信号,我们安装事件过滤器 lDock->installEventFilter(mainWindow); btmDock->installEventFilter(mainWindow); 然后,在mainWindow的事件过滤器中: bool MainWindow::eventFilter(QObject *watched, QEvent *event) { if (watched == lDock && event->type() == QEvent::Resize) { newDock->setFixedWidth(lDock->width()); } else if (watched == btmDock && event->type() == QEvent::Resize) { newDock->setFixedHeight(btmDock->height()); } return QMainWindow::eventFilter(watched, event); } 40. 注意:当newDock被拖动浮动时,我们不应该设置固定尺寸,所以需要判断newDock是否浮动。 在事件过滤器中,我们可以检查newDock的isFloating(),如果为true,则不设置。 41. 另外,当newDock被重新停靠时,我们可能需要重新设置固定尺寸。 42. 因此,我们还需要监听newDock的topLevelChanged信号: connect(newDock, &QDockWidget::topLevelChanged, [=](bool floating) { if (!floating) { newDock->setFixedWidth(lDock->width()); newDock->setFixedHeight(btmDock->height()); } }); 43. 这样,当newDock被重新停靠时,也会调整尺寸。 44. 但是,设置固定尺寸后,用户将无法通过拖动分割器来调整newDock的大小,这符合我们的要求(因为宽度和高度都是固定的,分别由lDock和btmDock决定)。 45. 最后,注意newDock的标题栏可能会显示,如果不需要,可以设置其特性。 46. 总结:通过设置左下角为左侧区域,然后按顺序添加dock,并动态调整newDock的尺寸,我们可以实现所需效果。 47. 注意:这种方法依赖于Qt的区域角落设置,可能在不同Qt版本中表现一致。 48. 代码示例: MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { // 创建dock部件 lDock = new QDockWidget("Left", this); btmDock = new QDockWidget("Bottom", this); newDock = new QDockWidget("New", this); // 设置内部widget(内容) lDock->setWidget(new QTextEdit("Left Dock")); btmDock->setWidget(new QTextEdit("Bottom Dock")); newDock->setWidget(new QTextEdit("New Dock")); // 设置左下角为左侧区域 setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); // 添加dock addDockWidget(Qt::LeftDockWidgetArea, lDock); addDockWidget(Qt::LeftDockWidgetArea, newDock); addDockWidget(Qt::BottomDockWidgetArea, btmDock); // 设置初始尺寸(在布局完成后) QTimer::singleShot(0, this, &MainWindow::adjustNewDockSize); // 安装事件过滤器 lDock->installEventFilter(this); btmDock->installEventFilter(this); connect(newDock, &QDockWidget::topLevelChanged, this, &MainWindow::onNewDockFloatingChanged); } void MainWindow::adjustNewDockSize() { if (!newDock->isFloating()) { newDock->setFixedWidth(lDock->width()); newDock->setFixedHeight(btmDock->height()); } } void MainWindow::onNewDockFloatingChanged(bool floating) { if (!floating) { adjustNewDockSize(); } } bool MainWindow::eventFilter(QObject *watched, QEvent *event) { if (event->type() == QEvent::Resize) { if (watched == lDock || watched == btmDock) { adjustNewDockSize(); } } return QMainWindow::eventFilter(watched, event); } 49. 注意:在事件过滤器中,当lDock或btmDock调整大小时,我们调整newDock的尺寸。但是,当调整newDock的尺寸时,它又会触发resize事件,但我们不会处理,因为watched不是lDock或btmDock。 50. 这样,我们就实现了需求。 51. 最后,如果用户拖动分割器调整lDock或btmDock的大小,newDock的尺寸会自动调整。 52. 但是,如果用户拖动newDock与其他dock的边界,由于我们设置了固定尺寸,用户将无法拖动。所以,我们需要在用户拖动时暂时取消固定尺寸,并在拖动结束后恢复?但这样会复杂化。根据需求,我们不需要用户调整newDock的尺寸,所以固定尺寸是可以接受的。 53. 因此,最终方案如上。 54. 如果遇到问题,可以调整事件过滤器的实现,比如使用resize事件来调整,并注意避免递归。 55. 另外,当主窗口首次显示时,可能还没有获得正确的尺寸,所以使用singleShot来延迟设置是必要的。 56. 如果布局在初始化后没有正确显示,可以尝试调用mainWindow->resizeDocks({newDock}, {lDock->width()}, Qt::Horizontal); 和 mainWindow->resizeDocks({newDock}, {btmDock->height()}, Qt::Vertical); 但是,这需要Qt5.6以上版本。 57. 综上,我们使用setCorner设置左下角为左侧区域,然后按顺序添加dock,并通过事件过滤器动态调整newDock的尺寸,实现所需效果。</think>在Qt中实现一个QDockWidget同时位于左侧和底部区域(即左下角),并精确控制其尺寸,可以通过以下步骤实现。关键在于利用`setCorner()`函数控制区域归属,并结合动态尺寸调整: ### 解决方案 #### 1. 设置区域归属 ```cpp // 设置左下角属于左侧停靠区域 mainWindow->setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); ``` - 此操作确保左侧区域可延伸到窗口左下角[^3] - 底部区域将占据右下角空间 #### 2. 添加DockWidget并设置布局 ```cpp // 添加左侧dock(lDock) mainWindow->addDockWidget(Qt::LeftDockWidgetArea, lDock); // 添加新dock(newDock)到左侧区域 mainWindow->addDockWidget(Qt::LeftDockWidgetArea, newDock); // 添加底部dock(btmDock) mainWindow->addDockWidget(Qt::BottomDockWidgetArea, btmDock); ``` - 添加顺序决定层级关系:后添加的dock会紧邻前一个dock[^1] - 此时布局效果: ``` +-------------------+-----------------------+ | | | | lDock | | | | | +-------------------+ | | | | | newDock | 中央区域 | | (左下角,未设置尺寸) | | +-------------------+-----------------------+ | btmDock (仅右侧部分显示) | +-------------------------------------------+ ``` #### 3. 动态尺寸调整 ```cpp // 在布局完成后设置初始尺寸 QTimer::singleShot(0, [=]() { if(!newDock->isFloating()) { newDock->setFixedWidth(lDock->width()); newDock->setFixedHeight(btmDock->height()); } }); // 监听尺寸变化(使用事件过滤器) bool MainWindow::eventFilter(QObject *watched, QEvent *event) { if (event->type() == QEvent::Resize) { if (watched == lDock) { newDock->setFixedWidth(lDock->width()); } else if (watched == btmDock) { newDock->setFixedHeight(btmDock->height()); } } return QMainWindow::eventFilter(watched, event); } // 在构造函数中安装事件过滤器 lDock->installEventFilter(this); btmDock->installEventFilter(this); // 处理浮动状态变化 connect(newDock, &QDockWidget::topLevelChanged, [=](bool floating) { if (!floating) { newDock->setFixedWidth(lDock->width()); newDock->setFixedHeight(btmDock->height()); } }); ``` ### 关键实现原理 1. **区域控制机制** - `setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea)` 使左侧区域占据左下角[^3] - 底部区域自动调整为仅占据右下角空间 - newDock添加在lDock之后,自动位于其下方[^1] 2. **尺寸同步技术** - `setFixedWidth/Height()` 锁定尺寸(需在布局完成后调用) - 事件过滤器捕获关联dock的resize事件 - 处理浮动状态防止固定尺寸干扰 3. **最终布局效果** ``` +-------------------+-----------------------+ | | | | lDock | | | | | +-------------------+ 中央区域 | | newDock | | | 宽度=lDock | | | 高度=btmDock | | +---------+---------+-----------------------+ | | btmDock | | | newDock | (右侧) | | | (左下角) | | | +---------+---------+-----------------------+ ``` - newDock同时占据: - 左侧区域的下部(lDock下方) - 底部区域的左部(btmDock左侧) - 尺寸特性: - 宽度始终等于lDock宽度 - 高度始终等于btmDock高度 > ⚠️ **注意**:当newDock浮动时,应解除尺寸约束(示例代码已处理) ### 替代方案(不使用setCorner) 若需更精确控制,可使用分割操作: ```cpp // 垂直分割左侧区域 mainWindow->splitDockWidget(lDock, newDock, Qt::Vertical); // 创建占位dock QDockWidget* placeholder = new QDockWidget; placeholder->setFeatures(QDockWidget::NoDockWidgetFeatures); mainWindow->addDockWidget(Qt::BottomDockWidgetArea, placeholder); // 水平分割底部区域 mainWindow->splitDockWidget(placeholder, newDock, Qt::Horizontal); mainWindow->splitDockWidget(placeholder, btmDock, Qt::Horizontal); // 移除占位dock placeholder->deleteLater(); ``` 此方案通过占位dock实现精确位置控制,但需要额外处理尺寸同步[^1]。
阅读全文

相关推荐

用python做一个电脑win10的软件。 1.界面风格简洁现代,主色调为浅灰和白色,辅以蓝色和绿色点缀,突出操作重点,提升视觉舒适度。 界面元素间距充足,按钮和输入框大小适中,适合鼠标操作和快捷键使用。 顶部或侧边设有明显的步骤导航条,清晰展示当前流程阶段,支持点击返回上一步修改。 支持高分辨率显示,界面响应流畅。 常驻命令设置和操作历史区域设计简洁,方便随时访问。 2. 2.1. 导入表格 用户通过“导入”按钮或拖拽区域选择或批量导入Excel文件(.xls/.xlsx),最多支持同时打开30个及以上文件。 导入后自动识别表格内容格式(数字、日期、文本),并生成分析报告。 支持文件列表展示,显示文件名、大小、导入时间及格式识别结果。 2.2. 分析表格内容及格式 自动分析导入的表格内容和格式,结果以图表和统计数据形式可视化展示。 支持自动修正格式错误(如错误的日期格式、数字格式不统一等),用户可选择应用或忽略。 分析结果支持导出报告。 2.3. 修改表格内容及格式 分析完成后自动进入编辑界面,展示表格内容。 支持直接编辑单元格内容,支持批量修改(如根据命令条件批量替换或归类)。 支持撤销和重做操作。 支持多标签页同时编辑多个表格,便于快速切换。 2.4. 合并表格 用户可选择结构相同的多个表格文件进行合并。 合并规则为简单拼接行,合并后整行用不同颜色标记,颜色方案支持用户自定义。 支持预览合并结果,确认后保存为新文件。 支持取消合并操作。 2.5. 导出表格及重命名 支持选择导出格式(如xlsx、csv等)。 支持批量重命名,支持自定义规则(如添加时间戳、序号等)。 导出后文件保存为新文件,避免覆盖原文件。 2.6. 命令设置(常驻功能) 命令条件专注于多国语言同义词归类和同属性词归列。 支持导入词库文件,方便批量管理同义词和属性词。 提供命令条件的预览和测试功能,方便用户验证规则效果。 命令条件应用于导入、分析、编辑和合并流程,实现智能内容处理。 2.7. 操作历史(常驻功能) 自动记录用户所有操作(导入、编辑、合并、导出等)。 支持按时间和操作类型筛选历史记录。 支持恢复到任意历史状态。 支持导出操作历史日志,便于审计和备份。 3. 软件启动后进入“导入表格”页面,用户通过顶部步骤导航条依次完成导入、分析、编辑、合并、导出五个步骤。 步骤导航条支持用户随时返回任一步骤修改内容。 “命令设置”和“操作历史”作为常驻功能模块,界面侧边或底部常显,用户可随时访问和操作,且命令条件影响所有流程步骤。 各步骤页面之间通过步骤导航条和界面内按钮顺畅切换,确保流程闭环。 软件为单窗口多页面设计,支持多标签页编辑多个表格,支持快捷键操作提升效率。

首先的是一个数据采集处理和导出的程序import sys import time import random import pandas as pd from datetime import datetime from PyQt5.QtCore import QThread, pyqtSignal, QMutex, QWaitCondition, Qt from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QTableWidget, QTableWidgetItem, QProgressBar, QLabel, QMessageBox, QTextEdit, QListWidget, QSplitter, QListWidgetItem, QAbstractItemView) 线程安全的数据存储 class ThreadSafeDataStore: def init(self): self.data = [] self.mutex = QMutex() self.condition = QWaitCondition() def add_data(self, item): """添加数据(线程安全)""" self.mutex.lock() self.data.append(item) self.mutex.unlock() def get_all_data(self): """获取所有数据(线程安全)""" self.mutex.lock() data_copy = self.data[:] self.mutex.unlock() return data_copy def clear(self): """清空数据(线程安全)""" self.mutex.lock() self.data.clear() self.mutex.unlock() 数据采集线程 class DataCollectorThread(QThread): data_collected = pyqtSignal(dict) progress_updated = pyqtSignal(int) status_changed = pyqtSignal(str) def __init__(self, data_store): super().__init__() self.data_store = data_store self._is_paused = False self._is_running = False self.pause_condition = QWaitCondition() self.mutex = QMutex() self.sample_count = 0 # 样本计数器 def run(self): """线程主循环""" self._is_running = True self.sample_count = 0 while self._is_running: # 暂停处理 self.mutex.lock() if self._is_paused: self.status_changed.emit("采集已暂停") self.pause_condition.wait(self.mutex) self.mutex.unlock() # 模拟数据采集 self.sample_count += 1 timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") value = random.uniform(0, 100) temperature = random.uniform(20, 30) data_point = { "timestamp": timestamp, "sample_id": self.sample_count, "value": value, "temperature": temperature } # 保存数据并发送信号 self.data_store.add_data(data_point) self.data_collected.emit(data_point) self.progress_updated.emit(self.sample_count) self.status_changed.emit(f"采集样本 #{self.sample_count}") # 控制采集速度 time.sleep(0.5) def pause(self): """暂停采集""" self.mutex.lock() self._is_paused = True self.mutex.unlock() def resume(self): """恢复采集""" self.mutex.lock() self._is_paused = False self.pause_condition.wakeAll() self.mutex.unlock() def stop(self): """停止线程""" self._is_running = False self.resume() # 确保线程能退出 self.wait() def reset_counter(self): """重置样本计数器""" self.sample_count = 0 数据处理线程 class DataProcessorThread(QThread): processed_data = pyqtSignal(dict) status_changed = pyqtSignal(str) def __init__(self, data_store): super().__init__() self.data_store = data_store self._is_running = False def run(self): """线程主循环""" self._is_running = True while self._is_running: # 获取所有数据 data = self.data_store.get_all_data() if data: # 模拟数据处理:计算统计指标 values = [d['value'] for d in data] avg_value = sum(values) / len(values) max_value = max(values) min_value = min(values) result = { "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "samples": len(data), "avg_value": avg_value, "max_value": max_value, "min_value": min_value } # 发送处理结果 self.processed_data.emit(result) self.status_changed.emit(f"处理了 {len(data)} 个样本") # 控制处理频率 time.sleep(1) def stop(self): """停止线程""" self._is_running = False self.wait() 主窗口 class MainWindow(QMainWindow): def init(self): super().init() self.setWindowTitle(“多线程数据采集与分析系统”) self.setGeometry(100, 100, 1000, 700) # 测试计数器 self.test_counter = 1 # 历史测试数据存储 self.history_tests = {} # 创建共享数据存储 self.data_store = ThreadSafeDataStore() # 初始化线程 self.collector_thread = DataCollectorThread(self.data_store) self.processor_thread = DataProcessorThread(self.data_store) # 初始化UI self.init_ui() # 连接信号 self.connect_signals() def init_ui(self): """初始化用户界面""" # 主布局 main_widget = QWidget() main_layout = QVBoxLayout(main_widget) # 使用分割器使界面更灵活 splitter = QSplitter(Qt.Vertical) # 顶部控制面板 top_widget = QWidget() top_layout = QVBoxLayout(top_widget) # 控制面板 - 使用两行布局 control_layout = QVBoxLayout() row1_layout = QHBoxLayout() row2_layout = QHBoxLayout() # 第一行按钮 self.start_btn = QPushButton("开始采集") self.pause_btn = QPushButton("暂停采集") self.stop_btn = QPushButton("停止采集") # 第二行按钮 self.restart_current_btn = QPushButton("重新本次采集") self.restart_btn = QPushButton("重新采集") self.export_btn = QPushButton("导出选中数据") # 初始状态设置 self.pause_btn.setEnabled(False) self.stop_btn.setEnabled(False) self.restart_current_btn.setEnabled(False) self.restart_btn.setEnabled(False) self.export_btn.setEnabled(False) # 添加到布局 row1_layout.addWidget(self.start_btn) row1_layout.addWidget(self.pause_btn) row1_layout.addWidget(self.stop_btn) row2_layout.addWidget(self.restart_current_btn) row2_layout.addWidget(self.restart_btn) row2_layout.addWidget(self.export_btn) control_layout.addLayout(row1_layout) control_layout.addLayout(row2_layout) # 状态显示 status_layout = QHBoxLayout() self.status_label = QLabel("就绪") self.progress_bar = QProgressBar() self.progress_bar.setRange(0, 100) status_layout.addWidget(self.status_label, 3) status_layout.addWidget(self.progress_bar, 7) top_layout.addLayout(control_layout) top_layout.addLayout(status_layout) # 中部数据显示区域 center_widget = QWidget() center_layout = QHBoxLayout(center_widget) # 左侧:历史测试列表 history_layout = QVBoxLayout() history_layout.addWidget(QLabel("历史测试记录:")) self.test_list = QListWidget() self.test_list.setSelectionMode(QAbstractItemView.ExtendedSelection) # 允许多选 history_layout.addWidget(self.test_list) # 右侧:数据表格 data_layout = QVBoxLayout() # 原始数据表格 self.data_table = QTableWidget() self.data_table.setColumnCount(4) self.data_table.setHorizontalHeaderLabels(["测试时间", "测试次数", "指标1", "指标2"]) self.data_table.setMinimumHeight(200) # 处理结果表格 self.result_table = QTableWidget() self.result_table.setColumnCount(5) self.result_table.setHorizontalHeaderLabels(["处理时间", "样本数", "平均值", "最大值", "最小值"]) self.result_table.setMinimumHeight(150) data_layout.addWidget(QLabel("原始数据:")) data_layout.addWidget(self.data_table) data_layout.addWidget(QLabel("处理结果:")) data_layout.addWidget(self.result_table) center_layout.addLayout(history_layout, 1) center_layout.addLayout(data_layout, 3) # 底部:日志区域 bottom_widget = QWidget() bottom_layout = QVBoxLayout(bottom_widget) bottom_layout.addWidget(QLabel("操作日志:")) self.log_view = QTextEdit() self.log_view.setReadOnly(True) self.log_view.setMinimumHeight(100) bottom_layout.addWidget(self.log_view) # 组装分割器 splitter.addWidget(top_widget) splitter.addWidget(center_widget) splitter.addWidget(bottom_widget) splitter.setSizes([100, 400, 150]) # 设置各部分大小比例 main_layout.addWidget(splitter) self.setCentralWidget(main_widget) # 连接按钮信号 self.start_btn.clicked.connect(self.start_test) self.pause_btn.clicked.connect(self.toggle_pause) self.stop_btn.clicked.connect(self.stop_test) self.export_btn.clicked.connect(self.export_data) self.restart_current_btn.clicked.connect(self.restart_current) self.restart_btn.clicked.connect(self.restart_all) # 连接列表选择信号 self.test_list.itemSelectionChanged.connect(self.show_selected_test) def connect_signals(self): """连接线程信号到槽函数""" self.collector_thread.data_collected.connect(self.add_data_row) self.collector_thread.progress_updated.connect(self.update_progress) self.collector_thread.status_changed.connect(self.update_status) self.processor_thread.processed_data.connect(self.add_result_row) self.processor_thread.status_changed.connect(self.log_message) def start_test(self): """开始采集""" self.log_message("开始数据采集...") # 清空数据 self.data_store.clear() self.data_table.setRowCount(0) self.result_table.setRowCount(0) self.progress_bar.setValue(0) # 重置样本计数器 self.collector_thread.reset_counter() # 启动线程 if not self.collector_thread.isRunning(): self.collector_thread.start() if not self.processor_thread.isRunning(): self.processor_thread.start() # 更新UI状态 self.start_btn.setEnabled(False) self.pause_btn.setEnabled(True) self.stop_btn.setEnabled(True) self.restart_current_btn.setEnabled(True) self.restart_btn.setEnabled(True) self.export_btn.setEnabled(False) self.pause_btn.setText("暂停采集") def restart_current(self): """重新本次采集 - 清空当前数据但保持采集状态""" if not self.collector_thread.isRunning(): self.log_message("警告:采集未运行,无法重新本次采集") return self.log_message("重新本次采集...") # 清空当前数据 self.data_store.clear() self.data_table.setRowCount(0) self.result_table.setRowCount(0) self.progress_bar.setValue(0) # 重置样本计数器 self.collector_thread.reset_counter() self.log_message("已清空当前采集数据,继续采集...") def restart_all(self): """重新采集 - 停止当前采集并重新开始""" self.log_message("重新采集...") # 停止当前采集 if self.collector_thread.isRunning(): self.stop_test() # 开始新的采集 self.start_test() def toggle_pause(self): """切换暂停/继续状态""" if not self.collector_thread.isRunning(): self.log_message("警告:采集未运行,无法暂停") return if self.collector_thread._is_paused: self.collector_thread.resume() self.pause_btn.setText("暂停采集") self.log_message("继续数据采集...") else: self.collector_thread.pause() self.pause_btn.setText("继续采集") self.log_message("数据采集已暂停...") def stop_test(self): """停止采集""" self.log_message("停止数据采集...") # 停止线程 if self.collector_thread.isRunning(): self.collector_thread.stop() if self.processor_thread.isRunning(): self.processor_thread.stop() # 保存本次测试数据 test_name = f"测试{self.test_counter}" test_data = self.data_store.get_all_data() self.history_tests[test_name] = test_data # 添加到历史测试列表 item = QListWidgetItem(test_name) item.setData(Qt.UserRole, test_name) # 存储测试名称 self.test_list.addItem(item) self.log_message(f"已保存本次测试数据: {test_name}") self.test_counter += 1 # 更新UI状态 self.start_btn.setEnabled(True) self.pause_btn.setEnabled(False) self.stop_btn.setEnabled(False) self.restart_current_btn.setEnabled(False) self.restart_btn.setEnabled(True) self.export_btn.setEnabled(True) self.pause_btn.setText("暂停采集") # 重置按钮文本 data_count = len(test_data) self.log_message(f"采集完成,共收集 {data_count} 个样本") def show_selected_test(self): """显示选中的测试数据""" selected_items = self.test_list.selectedItems() if not selected_items: return # 只显示第一个选中的测试 test_name = selected_items[0].data(Qt.UserRole) test_data = self.history_tests.get(test_name, []) # 更新原始数据表格 self.data_table.setRowCount(0) for row, data_point in enumerate(test_data): self.data_table.insertRow(row) self.data_table.setItem(row, 0, QTableWidgetItem(data_point["timestamp"])) self.data_table.setItem(row, 1, QTableWidgetItem(str(data_point["sample_id"]))) self.data_table.setItem(row, 2, QTableWidgetItem(f"{data_point['value']:.2f}")) self.data_table.setItem(row, 3, QTableWidgetItem(f"{data_point['temperature']:.2f}")) # 更新处理结果表格(需要重新计算) if test_data: values = [d['value'] for d in test_data] avg_value = sum(values) / len(values) max_value = max(values) min_value = min(values) self.result_table.setRowCount(0) self.result_table.insertRow(0) self.result_table.setItem(0, 0, QTableWidgetItem(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))) self.result_table.setItem(0, 1, QTableWidgetItem(str(len(test_data)))) self.result_table.setItem(0, 2, QTableWidgetItem(f"{avg_value:.2f}")) self.result_table.setItem(0, 3, QTableWidgetItem(f"{max_value:.2f}")) self.result_table.setItem(0, 4, QTableWidgetItem(f"{min_value:.2f}")) def export_data(self): """导出选中的测试数据到Excel文件""" selected_items = self.test_list.selectedItems() if not selected_items: QMessageBox.warning(self, "导出失败", "请选择要导出的测试集") return # 收集所有选中的测试数据 all_data = [] for item in selected_items: test_name = item.data(Qt.UserRole) test_data = self.history_tests.get(test_name, []) # 为每个数据点添加测试名称 for data_point in test_data: data_point_with_test = data_point.copy() data_point_with_test["test_name"] = test_name all_data.append(data_point_with_test) if not all_data: QMessageBox.warning(self, "导出失败", "选中的测试集没有可导出的数据") return try: today = datetime.now() filename = f"data_export_{today.strftime('%Y%m%d_%H%M%S')}.xlsx" # 创建DataFrame并导出 df = pd.DataFrame(all_data) # 重新排序列 column_order = ["test_name", "timestamp", "sample_id", "value", "temperature"] df = df[column_order] df.to_excel(filename, index=False) self.log_message(f"已导出 {len(selected_items)} 个测试集的数据到: {filename}") QMessageBox.information(self, "导出成功", f"已成功导出 {len(selected_items)} 个测试集的数据到:\n{filename}") except Exception as e: self.log_message(f"导出失败: {str(e)}") QMessageBox.critical(self, "导出错误", f"导出数据时发生错误:\n{str(e)}") def add_data_row(self, data): """添加数据到表格""" row = self.data_table.rowCount() self.data_table.insertRow(row) self.data_table.setItem(row, 0, QTableWidgetItem(data["timestamp"])) self.data_table.setItem(row, 1, QTableWidgetItem(str(data["sample_id"]))) self.data_table.setItem(row, 2, QTableWidgetItem(f"{data['value']:.2f}")) self.data_table.setItem(row, 3, QTableWidgetItem(f"{data['temperature']:.2f}")) # 自动滚动到最后一行 self.data_table.scrollToBottom() def add_result_row(self, result): """添加处理结果到表格""" row = self.result_table.rowCount() self.result_table.insertRow(row) self.result_table.setItem(row, 0, QTableWidgetItem(result["timestamp"])) self.result_table.setItem(row, 1, QTableWidgetItem(str(result["samples"]))) self.result_table.setItem(row, 2, QTableWidgetItem(f"{result['avg_value']:.2f}")) self.result_table.setItem(row, 3, QTableWidgetItem(f"{result['max_value']:.2f}")) self.result_table.setItem(row, 4, QTableWidgetItem(f"{result['min_value']:.2f}")) # 自动滚动到最后一行 self.result_table.scrollToBottom() def update_progress(self, count): """更新进度条""" self.progress_bar.setValue(count % 100) def update_status(self, status): """更新状态标签""" self.status_label.setText(status) def log_message(self, message): """添加日志消息""" timestamp = datetime.now().strftime("%H:%M:%S") self.log_view.append(f"[{timestamp}] {message}") def closeEvent(self, event): """窗口关闭时确保线程停止""" if self.collector_thread.isRunning(): self.collector_thread.stop() if self.processor_thread.isRunning(): self.processor_thread.stop() event.accept() if name == “main”: app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_()) 接下来是需要修改的程序,请根据上面程序的内容,修改下面程序的数据采集后的部分,仿照上面的程序在测试后建立测试集,选中测设集可以导出为excel文件。不要修改原界面布局,界面中间部分的下方空白处放的是采集的数据,右上方的空白处显示数据集,将“导出全部为json”改为“导出为Excel”,点击此按钮即可导出excel文件,其他的部分仍要保持原样 import sys from PyQt5.QtGui import QPixmap, QPainter from PyQt5.QtWidgets import ( QApplication, QMainWindow, QDockWidget, QVBoxLayout, QWidget, QLabel, QPushButton, QTreeWidget, QTreeWidgetItem, QTextEdit, QFileDialog, QMessageBox, QTextBrowser ) from PyQt5.QtCore import Qt, QTimer, QUrl, QThread, QDir from ui.Pages.capturePage.LeftRegion1 import StatusRegion from ui.Pages.capturePage.RightRegion import videoRegion, videoRegion1 from ui.Pages.capturePage.CenterUpRegion import CentralUpRegion from ui.Pages.capturePage.CenterDownRegion1 import CentralDownpRegion class CapturePage(QMainWindow): def init(self): super().init() self.init_ratio() self.initUI() self.slot_connect() def init_ratio(self): """初始化区域比例""" self.horizontal_ratio = [0.2, 0.8] # 左右区域 self.vertical_ratio = [0.5, 0.5] # 中部上下 self.right_ratio = [0.5, 0.5] # 右侧上下 def slot_connect(self): """连接信号槽""" self.status_ui.device_status_changed.connect(self.central_down_widget.get_device_status) self.status_ui.device_stream.connect(self.central_down_widget.handle_data_from_device) def initUI(self): self.takeCentralWidget() self.setDockOptions(self.dockOptions() | QMainWindow.DockOption.AllowTabbedDocks) self.setDockNestingEnabled(True) self.setWindowTitle("数据采集界面") self.setGeometry(100, 100, 1600, 1000) # 初始窗口大小 # 左侧区域 self.status_ui = StatusRegion() self.addDockWidget(Qt.LeftDockWidgetArea, self.status_ui) # 中间区域 self.central_up_widget = CentralUpRegion() # 右侧区域 self.video1_ui = videoRegion() self.central_down_widget = CentralDownpRegion(self.central_up_widget, self.video1_ui) self.video2_ui = videoRegion1() self.addDockWidget(Qt.RightDockWidgetArea, self.video1_ui) self.addDockWidget(Qt.RightDockWidgetArea, self.video2_ui) # 嵌套布局 self.splitDockWidget(self.status_ui, self.central_up_widget, Qt.Horizontal) self.splitDockWidget(self.central_up_widget, self.central_down_widget, Qt.Vertical) self.splitDockWidget(self.video1_ui, self.video2_ui, Qt.Vertical) # 背景图 self.background = QPixmap(r"C:\Users\86153\Downloads\【慧首UI】开发\素材\caiji.jpg") def paintEvent(self, event): painter = QPainter(self) painter.drawPixmap(self.rect(), self.background) def resizeEvent(self, event): """保持窗口缩放时区域比例不变""" super().resizeEvent(event) # 横向分配(左:状态,中:中心区域) total_width = self.width() left_width = int(total_width * self.horizontal_ratio[0]) center_width = int(total_width * self.horizontal_ratio[1]) self.resizeDocks([self.status_ui, self.central_up_widget], [left_width, center_width], Qt.Horizontal) # 中部纵向分配(上、下) center_total_height = self.central_up_widget.height() + self.central_down_widget.height() up_height = int(center_total_height * self.vertical_ratio[0]) down_height = int(center_total_height * self.vertical_ratio[1]) self.resizeDocks([self.central_up_widget, self.central_down_widget], [up_height, down_height], Qt.Vertical) # 右侧纵向分配(video1, video2) right_total_height = self.video1_ui.height() + self.video2_ui.height() video1_height = int(right_total_height * self.right_ratio[0]) video2_height = int(right_total_height * self.right_ratio[1]) self.resizeDocks([self.video1_ui, self.video2_ui], [video1_height, video2_height], Qt.Vertical) if name == ‘main’: app = QApplication(sys.argv) main_window = CapturePage() main_window.show() sys.exit(app.exec_()) 以下为程序中调用的程序 import sys from PyQt5.QtWidgets import QVBoxLayout, QWidget, QLabel, QHBoxLayout, QPushButton, QTextBrowser from PyQt5.QtCore import Qt from PyQt5.QtWidgets import * from CenterUpRegion import CentralUpRegion from tools.device.myoDevice1 import DataCollectorThread from VideoPlay1 import VideoPlayerThread from ui.Pages.capturePage.tools.VideoPlay import VideoPlayerThread class CentralDownpRegion(QDockWidget): def init(self, PlayRegion,parent=None): super().init(‘采集控制区域’) self.root_path = r’C:\Users\86153\Downloads\【慧首UI】开发\素材’# 视频文件目录 self.video_paths = [‘休息.mp4’,‘握拳.mp4’, ‘抓手机.mp4’, ‘捏杯盖.mp4’, ‘抓水杯.mp4’, ‘二指捏笔.mp4’, ‘伸食指.mp4’, ‘捏钥匙.mp4’] self.PlayRegion = PlayRegion # 视频播放(对应上边的那个类) self.initUI() self.mode = 0 # 采集模式(0=全部采集,1=单个动作) self.current_action = 0 # 当前动作索引 (0~6) self.current_repeat = 0 # 当前动作的采集次数 (0~5) self.target_action = 0 # 目标动作(单个采集模式下) self.target_repeats = 6 # 目标采集次数(默认6次) self.is_paused = False # 是否暂停 self.is_resting = False # 是否处于休息状态 def initUI(self): # 创建中心区域布局 self.lower_area_widget = QWidget() self.lower_area_layout = QVBoxLayout() self.collect_button_layout = QHBoxLayout() # 选择模式 self.mode_all = QRadioButton("全部采集") self.mode_single = QRadioButton("单个动作采集") self.mode_all.setChecked(True) self.collect_button_layout.addWidget(self.mode_all) self.collect_button_layout.addWidget(self.mode_single) # 选择动作(单个模式) self.action_label = QLabel("选择动作:") self.action_select = QSpinBox() self.action_select.setRange(1, 7) # 1~7 对应索引 0~6 self.collect_button_layout.addWidget(self.action_label) self.collect_button_layout.addWidget(self.action_select) # 选择采集次数(单个模式) self.repeat_label = QLabel("采集次数:") self.repeat_select = QSpinBox() self.repeat_select.setRange(1, 6) # 1~6 次 self.collect_button_layout.addWidget(self.repeat_label) self.collect_button_layout.addWidget(self.repeat_select) self.lower_area_layout.addLayout(self.collect_button_layout) # 采集控制按钮 self.control_button_layout = QHBoxLayout() self.start_button = QPushButton("开始采集") self.pause_button = QPushButton("暂停采集") self.resume_button = QPushButton("继续采集") self.restart_current_button = QPushButton("重新本次采集") self.restart_button = QPushButton("重新采集") self.control_button_layout.addWidget(self.start_button) self.control_button_layout.addWidget(self.pause_button) self.control_button_layout.addWidget(self.resume_button) self.control_button_layout.addWidget(self.restart_current_button) self.control_button_layout.addWidget(self.restart_button) self.lower_area_layout.addLayout(self.control_button_layout) # 采集状态显示 self.collect_status = QTextBrowser() self.lower_area_layout.addWidget(self.collect_status) self.lower_area_widget.setLayout(self.lower_area_layout) self.setWidget(self.lower_area_widget) # 绑定按钮事件 self.start_button.clicked.connect(self.start_collection) self.pause_button.clicked.connect(self.pause_collection) self.resume_button.clicked.connect(self.resume_collection) self.restart_current_button.clicked.connect(self.restart_current_collection) self.restart_button.clicked.connect(self.restart_collection) # 模式切换 self.mode_all.toggled.connect(self.update_mode) self.mode_single.toggled.connect(self.update_mode) # 禁用“继续采集”按钮,只有在暂停后才能点击 self.resume_button.setEnabled(False) def update_mode(self): """ 更新模式选择 """ if self.mode_all.isChecked(): self.mode = 0 self.action_select.setEnabled(False) self.repeat_select.setEnabled(False) else: self.mode = 1 self.action_select.setEnabled(True) self.repeat_select.setEnabled(True) def start_collection(self): self.current_action = self.action_select.value() - 1 self.start_button.setEnabled(False) self.pause_button.setEnabled(True) self.resume_button.setEnabled(False) self.restart_current_button.setEnabled(True) self.restart_button.setEnabled(True) self.thread = VideoPlayerThread(self.root_path,self.video_paths) # 创建线程 self.thread.frame_ready.connect(self.PlayRegion.update_frame) self.thread.start_playback(self.mode,self.current_action,1) def pause_collection(self): self.collect_status.append("采集暂停...") self.start_button.setEnabled(False) self.pause_button.setEnabled(False) self.resume_button.setEnabled(True) self.restart_current_button.setEnabled(True) self.restart_button.setEnabled(True) self.thread.pause() def resume_collection(self): self.collect_status.append(f"恢复采集,继续动作 {self.current_action + 1},第 {self.current_repeat} 次采集...") self.start_button.setEnabled(False) self.pause_button.setEnabled(True) self.resume_button.setEnabled(False) self.restart_current_button.setEnabled(True) self.restart_button.setEnabled(True) self.thread.resume() def restart_current_collection(self): self.collect_status.append(f"重新开始当前动作 {self.current_action + 1}...") self.start_button.setEnabled(True) self.pause_button.setEnabled(False) self.resume_button.setEnabled(False) self.restart_current_button.setEnabled(True) self.restart_button.setEnabled(True) self.thread.stop() def restart_collection(self): self.collect_status.append("重新开始所有采集...") self.start_button.setEnabled(True) self.pause_button.setEnabled(False) self.resume_button.setEnabled(False) self.restart_current_button.setEnabled(True) self.restart_button.setEnabled(True) self.thread.stop() class MainWindow(QMainWindow): def init(self): super().init() self.setWindowTitle(“视频采集系统”) self.PlayRegion = CentralUpRegion(self) self.collect_control = CentralDownpRegion(self.PlayRegion) self.addDockWidget(Qt.TopDockWidgetArea, self.PlayRegion) self.addDockWidget(Qt.BottomDockWidgetArea, self.collect_control) if name == ‘main’: app = QApplication(sys.argv) main_window = MainWindow() main_window.show() sys.exit(app.exec_()) 以及 from PyQt5.QtCore import Qt from PyQt5.QtGui import QPixmap from PyQt5.QtWidgets import QLabel, QWidget, QVBoxLayout, QDockWidget class CentralUpRegion(QDockWidget): def init(self, parent=None): super().init(‘视频播放区域’, parent) # self.root_path = ‘D:/Code/UI/【慧首UI】开发/素材’ # 视频文件目录 # self.video_paths = [‘休息.mp4’,‘握拳.mp4’, ‘抓手机.mp4’, ‘捏杯盖.mp4’, ‘抓水杯.mp4’, ‘二指捏笔.mp4’, ‘伸食指.mp4’, # ‘捏钥匙.mp4’] self.current_video_index = 0 self.play_count = 0 self.play_limit = None self.mode = 0 self.action_index = None self.cap = None self.video_label = QLabel() self.initUI() # self.videoPlayThread.frame_ready.connect(self.update_frame) def initUI(self): # self.video_label = QLabel() self.video_label.setAlignment(Qt.AlignCenter) self.video_label.setText("等待播放视频...") self.widget = QWidget() self.layout = QVBoxLayout() self.layout.addWidget(self.video_label) self.widget.setLayout(self.layout) self.setWidget(self.widget) def update_frame(self,qimg): # print("接收到视频帧") self.video_label.setPixmap(QPixmap.fromImage(qimg))

首先的是一个数据采集处理和导出的程序import sys import time import random import pandas as pd from datetime import datetime from PyQt5.QtCore import QThread, pyqtSignal, QMutex, QWaitCondition, Qt from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QTableWidget, QTableWidgetItem, QProgressBar, QLabel, QMessageBox, QTextEdit, QListWidget, QSplitter, QListWidgetItem, QAbstractItemView) # 线程安全的数据存储 class ThreadSafeDataStore: def __init__(self): self.data = [] self.mutex = QMutex() self.condition = QWaitCondition() def add_data(self, item): """添加数据(线程安全)""" self.mutex.lock() self.data.append(item) self.mutex.unlock() def get_all_data(self): """获取所有数据(线程安全)""" self.mutex.lock() data_copy = self.data[:] self.mutex.unlock() return data_copy def clear(self): """清空数据(线程安全)""" self.mutex.lock() self.data.clear() self.mutex.unlock() # 数据采集线程 class DataCollectorThread(QThread): data_collected = pyqtSignal(dict) progress_updated = pyqtSignal(int) status_changed = pyqtSignal(str) def __init__(self, data_store): super().__init__() self.data_store = data_store self._is_paused = False self._is_running = False self.pause_condition = QWaitCondition() self.mutex = QMutex() self.sample_count = 0 # 样本计数器 def run(self): """线程主循环""" self._is_running = True self.sample_count = 0 while self._is_running: # 暂停处理 self.mutex.lock() if self._is_paused: self.status_changed.emit("采集已暂停") self.pause_condition.wait(self.mutex) self.mutex.unlock() # 模拟数据采集 self.sample_count += 1 timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") value = random.uniform(0, 100) temperature = random.uniform(20, 30) data_point = { "timestamp": timestamp, "sample_id": self.sample_count, "value": value, "temperature": temperature } # 保存数据并发送信号 self.data_store.add_data(data_point) self.data_collected.emit(data_point) self.progress_updated.emit(self.sample_count) self.status_changed.emit(f"采集样本 #{self.sample_count}") # 控制采集速度 time.sleep(0.5) def pause(self): """暂停采集""" self.mutex.lock() self._is_paused = True self.mutex.unlock() def resume(self): """恢复采集""" self.mutex.lock() self._is_paused = False self.pause_condition.wakeAll() self.mutex.unlock() def stop(self): """停止线程""" self._is_running = False self.resume() # 确保线程能退出 self.wait() def reset_counter(self): """重置样本计数器""" self.sample_count = 0 # 数据处理线程 class DataProcessorThread(QThread): processed_data = pyqtSignal(dict) status_changed = pyqtSignal(str) def __init__(self, data_store): super().__init__() self.data_store = data_store self._is_running = False def run(self): """线程主循环""" self._is_running = True while self._is_running: # 获取所有数据 data = self.data_store.get_all_data() if data: # 模拟数据处理:计算统计指标 values = [d['value'] for d in data] avg_value = sum(values) / len(values) max_value = max(values) min_value = min(values) result = { "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "samples": len(data), "avg_value": avg_value, "max_value": max_value, "min_value": min_value } # 发送处理结果 self.processed_data.emit(result) self.status_changed.emit(f"处理了 {len(data)} 个样本") # 控制处理频率 time.sleep(1) def stop(self): """停止线程""" self._is_running = False self.wait() # 主窗口 class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("多线程数据采集与分析系统") self.setGeometry(100, 100, 1000, 700) # 测试计数器 self.test_counter = 1 # 历史测试数据存储 self.history_tests = {} # 创建共享数据存储 self.data_store = ThreadSafeDataStore() # 初始化线程 self.collector_thread = DataCollectorThread(self.data_store) self.processor_thread = DataProcessorThread(self.data_store) # 初始化UI self.init_ui() # 连接信号 self.connect_signals() def init_ui(self): """初始化用户界面""" # 主布局 main_widget = QWidget() main_layout = QVBoxLayout(main_widget) # 使用分割器使界面更灵活 splitter = QSplitter(Qt.Vertical) # 顶部控制面板 top_widget = QWidget() top_layout = QVBoxLayout(top_widget) # 控制面板 - 使用两行布局 control_layout = QVBoxLayout() row1_layout = QHBoxLayout() row2_layout = QHBoxLayout() # 第一行按钮 self.start_btn = QPushButton("开始采集") self.pause_btn = QPushButton("暂停采集") self.stop_btn = QPushButton("停止采集") # 第二行按钮 self.restart_current_btn = QPushButton("重新本次采集") self.restart_btn = QPushButton("重新采集") self.export_btn = QPushButton("导出选中数据") # 初始状态设置 self.pause_btn.setEnabled(False) self.stop_btn.setEnabled(False) self.restart_current_btn.setEnabled(False) self.restart_btn.setEnabled(False) self.export_btn.setEnabled(False) # 添加到布局 row1_layout.addWidget(self.start_btn) row1_layout.addWidget(self.pause_btn) row1_layout.addWidget(self.stop_btn) row2_layout.addWidget(self.restart_current_btn) row2_layout.addWidget(self.restart_btn) row2_layout.addWidget(self.export_btn) control_layout.addLayout(row1_layout) control_layout.addLayout(row2_layout) # 状态显示 status_layout = QHBoxLayout() self.status_label = QLabel("就绪") self.progress_bar = QProgressBar() self.progress_bar.setRange(0, 100) status_layout.addWidget(self.status_label, 3) status_layout.addWidget(self.progress_bar, 7) top_layout.addLayout(control_layout) top_layout.addLayout(status_layout) # 中部数据显示区域 center_widget = QWidget() center_layout = QHBoxLayout(center_widget) # 左侧:历史测试列表 history_layout = QVBoxLayout() history_layout.addWidget(QLabel("历史测试记录:")) self.test_list = QListWidget() self.test_list.setSelectionMode(QAbstractItemView.ExtendedSelection) # 允许多选 history_layout.addWidget(self.test_list) # 右侧:数据表格 data_layout = QVBoxLayout() # 原始数据表格 self.data_table = QTableWidget() self.data_table.setColumnCount(4) self.data_table.setHorizontalHeaderLabels(["测试时间", "测试次数", "指标1", "指标2"]) self.data_table.setMinimumHeight(200) # 处理结果表格 self.result_table = QTableWidget() self.result_table.setColumnCount(5) self.result_table.setHorizontalHeaderLabels(["处理时间", "样本数", "平均值", "最大值", "最小值"]) self.result_table.setMinimumHeight(150) data_layout.addWidget(QLabel("原始数据:")) data_layout.addWidget(self.data_table) data_layout.addWidget(QLabel("处理结果:")) data_layout.addWidget(self.result_table) center_layout.addLayout(history_layout, 1) center_layout.addLayout(data_layout, 3) # 底部:日志区域 bottom_widget = QWidget() bottom_layout = QVBoxLayout(bottom_widget) bottom_layout.addWidget(QLabel("操作日志:")) self.log_view = QTextEdit() self.log_view.setReadOnly(True) self.log_view.setMinimumHeight(100) bottom_layout.addWidget(self.log_view) # 组装分割器 splitter.addWidget(top_widget) splitter.addWidget(center_widget) splitter.addWidget(bottom_widget) splitter.setSizes([100, 400, 150]) # 设置各部分大小比例 main_layout.addWidget(splitter) self.setCentralWidget(main_widget) # 连接按钮信号 self.start_btn.clicked.connect(self.start_test) self.pause_btn.clicked.connect(self.toggle_pause) self.stop_btn.clicked.connect(self.stop_test) self.export_btn.clicked.connect(self.export_data) self.restart_current_btn.clicked.connect(self.restart_current) self.restart_btn.clicked.connect(self.restart_all) # 连接列表选择信号 self.test_list.itemSelectionChanged.connect(self.show_selected_test) def connect_signals(self): """连接线程信号到槽函数""" self.collector_thread.data_collected.connect(self.add_data_row) self.collector_thread.progress_updated.connect(self.update_progress) self.collector_thread.status_changed.connect(self.update_status) self.processor_thread.processed_data.connect(self.add_result_row) self.processor_thread.status_changed.connect(self.log_message) def start_test(self): """开始采集""" self.log_message("开始数据采集...") # 清空数据 self.data_store.clear() self.data_table.setRowCount(0) self.result_table.setRowCount(0) self.progress_bar.setValue(0) # 重置样本计数器 self.collector_thread.reset_counter() # 启动线程 if not self.collector_thread.isRunning(): self.collector_thread.start() if not self.processor_thread.isRunning(): self.processor_thread.start() # 更新UI状态 self.start_btn.setEnabled(False) self.pause_btn.setEnabled(True) self.stop_btn.setEnabled(True) self.restart_current_btn.setEnabled(True) self.restart_btn.setEnabled(True) self.export_btn.setEnabled(False) self.pause_btn.setText("暂停采集") def restart_current(self): """重新本次采集 - 清空当前数据但保持采集状态""" if not self.collector_thread.isRunning(): self.log_message("警告:采集未运行,无法重新本次采集") return self.log_message("重新本次采集...") # 清空当前数据 self.data_store.clear() self.data_table.setRowCount(0) self.result_table.setRowCount(0) self.progress_bar.setValue(0) # 重置样本计数器 self.collector_thread.reset_counter() self.log_message("已清空当前采集数据,继续采集...") def restart_all(self): """重新采集 - 停止当前采集并重新开始""" self.log_message("重新采集...") # 停止当前采集 if self.collector_thread.isRunning(): self.stop_test() # 开始新的采集 self.start_test() def toggle_pause(self): """切换暂停/继续状态""" if not self.collector_thread.isRunning(): self.log_message("警告:采集未运行,无法暂停") return if self.collector_thread._is_paused: self.collector_thread.resume() self.pause_btn.setText("暂停采集") self.log_message("继续数据采集...") else: self.collector_thread.pause() self.pause_btn.setText("继续采集") self.log_message("数据采集已暂停...") def stop_test(self): """停止采集""" self.log_message("停止数据采集...") # 停止线程 if self.collector_thread.isRunning(): self.collector_thread.stop() if self.processor_thread.isRunning(): self.processor_thread.stop() # 保存本次测试数据 test_name = f"测试{self.test_counter}" test_data = self.data_store.get_all_data() self.history_tests[test_name] = test_data # 添加到历史测试列表 item = QListWidgetItem(test_name) item.setData(Qt.UserRole, test_name) # 存储测试名称 self.test_list.addItem(item) self.log_message(f"已保存本次测试数据: {test_name}") self.test_counter += 1 # 更新UI状态 self.start_btn.setEnabled(True) self.pause_btn.setEnabled(False) self.stop_btn.setEnabled(False) self.restart_current_btn.setEnabled(False) self.restart_btn.setEnabled(True) self.export_btn.setEnabled(True) self.pause_btn.setText("暂停采集") # 重置按钮文本 data_count = len(test_data) self.log_message(f"采集完成,共收集 {data_count} 个样本") def show_selected_test(self): """显示选中的测试数据""" selected_items = self.test_list.selectedItems() if not selected_items: return # 只显示第一个选中的测试 test_name = selected_items[0].data(Qt.UserRole) test_data = self.history_tests.get(test_name, []) # 更新原始数据表格 self.data_table.setRowCount(0) for row, data_point in enumerate(test_data): self.data_table.insertRow(row) self.data_table.setItem(row, 0, QTableWidgetItem(data_point["timestamp"])) self.data_table.setItem(row, 1, QTableWidgetItem(str(data_point["sample_id"]))) self.data_table.setItem(row, 2, QTableWidgetItem(f"{data_point['value']:.2f}")) self.data_table.setItem(row, 3, QTableWidgetItem(f"{data_point['temperature']:.2f}")) # 更新处理结果表格(需要重新计算) if test_data: values = [d['value'] for d in test_data] avg_value = sum(values) / len(values) max_value = max(values) min_value = min(values) self.result_table.setRowCount(0) self.result_table.insertRow(0) self.result_table.setItem(0, 0, QTableWidgetItem(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))) self.result_table.setItem(0, 1, QTableWidgetItem(str(len(test_data)))) self.result_table.setItem(0, 2, QTableWidgetItem(f"{avg_value:.2f}")) self.result_table.setItem(0, 3, QTableWidgetItem(f"{max_value:.2f}")) self.result_table.setItem(0, 4, QTableWidgetItem(f"{min_value:.2f}")) def export_data(self): """导出选中的测试数据到Excel文件""" selected_items = self.test_list.selectedItems() if not selected_items: QMessageBox.warning(self, "导出失败", "请选择要导出的测试集") return # 收集所有选中的测试数据 all_data = [] for item in selected_items: test_name = item.data(Qt.UserRole) test_data = self.history_tests.get(test_name, []) # 为每个数据点添加测试名称 for data_point in test_data: data_point_with_test = data_point.copy() data_point_with_test["test_name"] = test_name all_data.append(data_point_with_test) if not all_data: QMessageBox.warning(self, "导出失败", "选中的测试集没有可导出的数据") return try: today = datetime.now() filename = f"data_export_{today.strftime('%Y%m%d_%H%M%S')}.xlsx" # 创建DataFrame并导出 df = pd.DataFrame(all_data) # 重新排序列 column_order = ["test_name", "timestamp", "sample_id", "value", "temperature"] df = df[column_order] df.to_excel(filename, index=False) self.log_message(f"已导出 {len(selected_items)} 个测试集的数据到: {filename}") QMessageBox.information(self, "导出成功", f"已成功导出 {len(selected_items)} 个测试集的数据到:\n{filename}") except Exception as e: self.log_message(f"导出失败: {str(e)}") QMessageBox.critical(self, "导出错误", f"导出数据时发生错误:\n{str(e)}") def add_data_row(self, data): """添加数据到表格""" row = self.data_table.rowCount() self.data_table.insertRow(row) self.data_table.setItem(row, 0, QTableWidgetItem(data["timestamp"])) self.data_table.setItem(row, 1, QTableWidgetItem(str(data["sample_id"]))) self.data_table.setItem(row, 2, QTableWidgetItem(f"{data['value']:.2f}")) self.data_table.setItem(row, 3, QTableWidgetItem(f"{data['temperature']:.2f}")) # 自动滚动到最后一行 self.data_table.scrollToBottom() def add_result_row(self, result): """添加处理结果到表格""" row = self.result_table.rowCount() self.result_table.insertRow(row) self.result_table.setItem(row, 0, QTableWidgetItem(result["timestamp"])) self.result_table.setItem(row, 1, QTableWidgetItem(str(result["samples"]))) self.result_table.setItem(row, 2, QTableWidgetItem(f"{result['avg_value']:.2f}")) self.result_table.setItem(row, 3, QTableWidgetItem(f"{result['max_value']:.2f}")) self.result_table.setItem(row, 4, QTableWidgetItem(f"{result['min_value']:.2f}")) # 自动滚动到最后一行 self.result_table.scrollToBottom() def update_progress(self, count): """更新进度条""" self.progress_bar.setValue(count % 100) def update_status(self, status): """更新状态标签""" self.status_label.setText(status) def log_message(self, message): """添加日志消息""" timestamp = datetime.now().strftime("%H:%M:%S") self.log_view.append(f"[{timestamp}] {message}") def closeEvent(self, event): """窗口关闭时确保线程停止""" if self.collector_thread.isRunning(): self.collector_thread.stop() if self.processor_thread.isRunning(): self.processor_thread.stop() event.accept() if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_()) 接下来是需要修改的程序,请根据上面程序的内容,修改下面程序的数据采集后的部分,仿照上面的程序在测试后建立测试集,选中测设集可以导出为excel文件。不要修改原界面布局,界面中间部分的下方空白处放的是采集的数据,右上方的空白处显示数据集,将“导出全部为json”改为“导出为Excel”,点击此按钮即可导出excel文件 import sys from PyQt5.QtGui import QPixmap, QPainter from PyQt5.QtWidgets import ( QApplication, QMainWindow, QDockWidget, QVBoxLayout, QWidget, QLabel, QPushButton, QTreeWidget, QTreeWidgetItem, QTextEdit, QFileDialog, QMessageBox, QTextBrowser ) from PyQt5.QtCore import Qt, QTimer, QUrl, QThread, QDir from ui.Pages.capturePage.LeftRegion1 import StatusRegion from ui.Pages.capturePage.RightRegion import videoRegion, videoRegion1 from ui.Pages.capturePage.CenterUpRegion import CentralUpRegion from ui.Pages.capturePage.CenterDownRegion1 import CentralDownpRegion class CapturePage(QMainWindow): def __init__(self): super().__init__() self.init_ratio() self.initUI() self.slot_connect() def init_ratio(self): """初始化区域比例""" self.horizontal_ratio = [0.2, 0.8] # 左右区域 self.vertical_ratio = [0.5, 0.5] # 中部上下 self.right_ratio = [0.5, 0.5] # 右侧上下 def slot_connect(self): """连接信号槽""" self.status_ui.device_status_changed.connect(self.central_down_widget.get_device_status) self.status_ui.device_stream.connect(self.central_down_widget.handle_data_from_device) def initUI(self): self.takeCentralWidget() self.setDockOptions(self.dockOptions() | QMainWindow.DockOption.AllowTabbedDocks) self.setDockNestingEnabled(True) self.setWindowTitle("数据采集界面") self.setGeometry(100, 100, 1600, 1000) # 初始窗口大小 # 左侧区域 self.status_ui = StatusRegion() self.addDockWidget(Qt.LeftDockWidgetArea, self.status_ui) # 中间区域 self.central_up_widget = CentralUpRegion() # 右侧区域 self.video1_ui = videoRegion() self.central_down_widget = CentralDownpRegion(self.central_up_widget, self.video1_ui) self.video2_ui = videoRegion1() self.addDockWidget(Qt.RightDockWidgetArea, self.video1_ui) self.addDockWidget(Qt.RightDockWidgetArea, self.video2_ui) # 嵌套布局 self.splitDockWidget(self.status_ui, self.central_up_widget, Qt.Horizontal) self.splitDockWidget(self.central_up_widget, self.central_down_widget, Qt.Vertical) self.splitDockWidget(self.video1_ui, self.video2_ui, Qt.Vertical) # 背景图 self.background = QPixmap(r"C:\Users\86153\Downloads\【慧首UI】开发\素材\caiji.jpg") def paintEvent(self, event): painter = QPainter(self) painter.drawPixmap(self.rect(), self.background) def resizeEvent(self, event): """保持窗口缩放时区域比例不变""" super().resizeEvent(event) # 横向分配(左:状态,中:中心区域) total_width = self.width() left_width = int(total_width * self.horizontal_ratio[0]) center_width = int(total_width * self.horizontal_ratio[1]) self.resizeDocks([self.status_ui, self.central_up_widget], [left_width, center_width], Qt.Horizontal) # 中部纵向分配(上、下) center_total_height = self.central_up_widget.height() + self.central_down_widget.height() up_height = int(center_total_height * self.vertical_ratio[0]) down_height = int(center_total_height * self.vertical_ratio[1]) self.resizeDocks([self.central_up_widget, self.central_down_widget], [up_height, down_height], Qt.Vertical) # 右侧纵向分配(video1, video2) right_total_height = self.video1_ui.height() + self.video2_ui.height() video1_height = int(right_total_height * self.right_ratio[0]) video2_height = int(right_total_height * self.right_ratio[1]) self.resizeDocks([self.video1_ui, self.video2_ui], [video1_height, video2_height], Qt.Vertical) if __name__ == '__main__': app = QApplication(sys.argv) main_window = CapturePage() main_window.show() sys.exit(app.exec_()) 以下为程序中调用的程序 import sys from PyQt5.QtWidgets import QVBoxLayout, QWidget, QLabel, \ QHBoxLayout, QPushButton, QTextBrowser from PyQt5.QtCore import Qt from PyQt5.QtWidgets import * from CenterUpRegion import CentralUpRegion # from tools.device.myoDevice1 import DataCollectorThread # from VideoPlay1 import VideoPlayerThread from ui.Pages.capturePage.tools.VideoPlay import VideoPlayerThread class CentralDownpRegion(QDockWidget): def __init__(self, PlayRegion,parent=None): super().__init__('采集控制区域') self.root_path = r'C:\Users\86153\Downloads\【慧首UI】开发\素材'# 视频文件目录 self.video_paths = ['休息.mp4','握拳.mp4', '抓手机.mp4', '捏杯盖.mp4', '抓水杯.mp4', '二指捏笔.mp4', '伸食指.mp4', '捏钥匙.mp4'] self.PlayRegion = PlayRegion # 视频播放(对应上边的那个类) self.initUI() self.mode = 0 # 采集模式(0=全部采集,1=单个动作) self.current_action = 0 # 当前动作索引 (0~6) self.current_repeat = 0 # 当前动作的采集次数 (0~5) self.target_action = 0 # 目标动作(单个采集模式下) self.target_repeats = 6 # 目标采集次数(默认6次) self.is_paused = False # 是否暂停 self.is_resting = False # 是否处于休息状态 def initUI(self): # 创建中心区域布局 self.lower_area_widget = QWidget() self.lower_area_layout = QVBoxLayout() self.collect_button_layout = QHBoxLayout() # 选择模式 self.mode_all = QRadioButton("全部采集") self.mode_single = QRadioButton("单个动作采集") self.mode_all.setChecked(True) self.collect_button_layout.addWidget(self.mode_all) self.collect_button_layout.addWidget(self.mode_single) # 选择动作(单个模式) self.action_label = QLabel("选择动作:") self.action_select = QSpinBox() self.action_select.setRange(1, 7) # 1~7 对应索引 0~6 self.collect_button_layout.addWidget(self.action_label) self.collect_button_layout.addWidget(self.action_select) # 选择采集次数(单个模式) self.repeat_label = QLabel("采集次数:") self.repeat_select = QSpinBox() self.repeat_select.setRange(1, 6) # 1~6 次 self.collect_button_layout.addWidget(self.repeat_label) self.collect_button_layout.addWidget(self.repeat_select) self.lower_area_layout.addLayout(self.collect_button_layout) # 采集控制按钮 self.control_button_layout = QHBoxLayout() self.start_button = QPushButton("开始采集") self.pause_button = QPushButton("暂停采集") self.resume_button = QPushButton("继续采集") self.restart_current_button = QPushButton("重新本次采集") self.restart_button = QPushButton("重新采集") self.control_button_layout.addWidget(self.start_button) self.control_button_layout.addWidget(self.pause_button) self.control_button_layout.addWidget(self.resume_button) self.control_button_layout.addWidget(self.restart_current_button) self.control_button_layout.addWidget(self.restart_button) self.lower_area_layout.addLayout(self.control_button_layout) # 采集状态显示 self.collect_status = QTextBrowser() self.lower_area_layout.addWidget(self.collect_status) self.lower_area_widget.setLayout(self.lower_area_layout) self.setWidget(self.lower_area_widget) # 绑定按钮事件 self.start_button.clicked.connect(self.start_collection) self.pause_button.clicked.connect(self.pause_collection) self.resume_button.clicked.connect(self.resume_collection) self.restart_current_button.clicked.connect(self.restart_current_collection) self.restart_button.clicked.connect(self.restart_collection) # 模式切换 self.mode_all.toggled.connect(self.update_mode) self.mode_single.toggled.connect(self.update_mode) # 禁用“继续采集”按钮,只有在暂停后才能点击 self.resume_button.setEnabled(False) def update_mode(self): """ 更新模式选择 """ if self.mode_all.isChecked(): self.mode = 0 self.action_select.setEnabled(False) self.repeat_select.setEnabled(False) else: self.mode = 1 self.action_select.setEnabled(True) self.repeat_select.setEnabled(True) def start_collection(self): self.current_action = self.action_select.value() - 1 self.start_button.setEnabled(False) self.pause_button.setEnabled(True) self.resume_button.setEnabled(False) self.restart_current_button.setEnabled(True) self.restart_button.setEnabled(True) self.thread = VideoPlayerThread(self.root_path,self.video_paths) # 创建线程 self.thread.frame_ready.connect(self.PlayRegion.update_frame) self.thread.start_playback(self.mode,self.current_action,1) def pause_collection(self): self.collect_status.append("采集暂停...") self.start_button.setEnabled(False) self.pause_button.setEnabled(False) self.resume_button.setEnabled(True) self.restart_current_button.setEnabled(True) self.restart_button.setEnabled(True) self.thread.pause() def resume_collection(self): self.collect_status.append(f"恢复采集,继续动作 {self.current_action + 1},第 {self.current_repeat} 次采集...") self.start_button.setEnabled(False) self.pause_button.setEnabled(True) self.resume_button.setEnabled(False) self.restart_current_button.setEnabled(True) self.restart_button.setEnabled(True) self.thread.resume() def restart_current_collection(self): self.collect_status.append(f"重新开始当前动作 {self.current_action + 1}...") self.start_button.setEnabled(True) self.pause_button.setEnabled(False) self.resume_button.setEnabled(False) self.restart_current_button.setEnabled(True) self.restart_button.setEnabled(True) self.thread.stop() def restart_collection(self): self.collect_status.append("重新开始所有采集...") self.start_button.setEnabled(True) self.pause_button.setEnabled(False) self.resume_button.setEnabled(False) self.restart_current_button.setEnabled(True) self.restart_button.setEnabled(True) self.thread.stop() class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("视频采集系统") self.PlayRegion = CentralUpRegion(self) self.collect_control = CentralDownpRegion(self.PlayRegion) self.addDockWidget(Qt.TopDockWidgetArea, self.PlayRegion) self.addDockWidget(Qt.BottomDockWidgetArea, self.collect_control) if __name__ == '__main__': app = QApplication(sys.argv) main_window = MainWindow() main_window.show() sys.exit(app.exec_()) 以及 from PyQt5.QtCore import Qt from PyQt5.QtGui import QPixmap from PyQt5.QtWidgets import QLabel, QWidget, QVBoxLayout, QDockWidget class CentralUpRegion(QDockWidget): def __init__(self, parent=None): super().__init__('视频播放区域', parent) # self.root_path = 'D:/Code/UI/【慧首UI】开发/素材' # 视频文件目录 # self.video_paths = ['休息.mp4','握拳.mp4', '抓手机.mp4', '捏杯盖.mp4', '抓水杯.mp4', '二指捏笔.mp4', '伸食指.mp4', # '捏钥匙.mp4'] self.current_video_index = 0 self.play_count = 0 self.play_limit = None self.mode = 0 self.action_index = None self.cap = None self.video_label = QLabel() self.initUI() # self.videoPlayThread.frame_ready.connect(self.update_frame) def initUI(self): # self.video_label = QLabel() self.video_label.setAlignment(Qt.AlignCenter) self.video_label.setText("等待播放视频...") self.widget = QWidget() self.layout = QVBoxLayout() self.layout.addWidget(self.video_label) self.widget.setLayout(self.layout) self.setWidget(self.widget) def update_frame(self,qimg): # print("接收到视频帧") self.video_label.setPixmap(QPixmap.fromImage(qimg))

最新推荐

recommend-type

PyQt5 QDockWidget控件应用详解

1. **停靠和浮动**:`QDockWidget`可以停靠在主窗口的左侧、右侧、顶部或底部,也可以选择浮动成为独立的窗口。通过调用`setFloating()`方法,可以设置`QDockWidget`是否浮动。例如,`setFloating(True)`会使`...
recommend-type

中孚密保卫士是由中孚信息股份有限公司开发的一款信息安全产品,主要用于终端计算机的保密管理和数据防泄漏 它主要面向政府机关、军工单位、科研院所等对信息安全有较高要求的涉密单位,帮助其实现对涉密信息的全

终端保密管理:对计算机终端的操作行为进行监控和审计,防止违规外联、非法拷贝、打印、截屏等行为。 数据防泄漏(DLP):通过内容识别、加密、权限控制等手段,防止敏感或涉密数据通过U盘、网络、邮件等途径泄露。 文件加密与权限控制:对涉密文件进行透明加密,确保文件在授权范围内使用,防止未授权人员查看或传播。 行为审计与日志记录:详细记录用户的操作行为(如文件访问、外发、打印等),便于事后追溯和审计。 违规外联监控:防止涉密计算机违规连接互联网或其他非授权网络,保障网络边界安全。 移动存储介质管理:对U盘、移动硬盘等设备进行授权管理,区分普通盘和专用盘,防止非法数据拷贝。
recommend-type

快速浏览Hacker News热门故事的浏览器扩展

Hacker News Browser-crx插件是一款专为浏览器设计的扩展程序,它允许用户从任何网页上浏览Hacker News上的热门故事,该网站是科技界尤其是编程和创业圈子中非常受欢迎的信息交流平台。Hacker News上的内容主要包括编程、科技创业、互联网趣闻以及相关的讨论。它由Y Combinator(一家知名的硅谷创业孵化器)所维护。 ### 关键知识点解析: 1. **扩展程序(Extension)**: - 扩展程序是一种软件,旨在为浏览器提供额外功能和定制选项。它们可以增强用户的浏览体验,提高效率和安全性。扩展程序通常开发于HTML、CSS和JavaScript技术栈,可以针对不同的浏览器开发,如Chrome、Firefox、Safari等。 2. **Hacker News简介**: - Hacker News(也称为Hacker News或者HN)是一个新闻社交网站,由Paul Graham和Trevor Blackwell等人于2007年发起,隶属于Y Combinator。它提供了一个平台,让用户分享、讨论技术新闻和创业公司的相关文章。Hacker News社区以其高质量的讨论和新闻而闻名,吸引了大量程序员、企业家和科技爱好者。 3. **Hacker News Browser-crx插件功能**: - **浏览过去24小时的热门故事**:插件允许用户查看Hacker News中最近24小时内的热门内容。这为用户提供了快速获取当前科技界热门话题的途径。 - **保存故事到Pocket**:Pocket是一个服务,允许用户保存文章、视频和网页以便离线阅读。Hacker News Browser-crx插件可以与用户的Pocket账户集成,方便用户保存他们感兴趣的内容到自己的Pocket列表中。 - **直接从扩展发推文**:社交媒体是现代信息传播的一个重要渠道。通过这个功能,用户可以将他们在Hacker News上的发现直接通过Twitter分享给他们的关注者。 - **搜索特定主题**:用户可以使用这个功能来搜索Hacker News上的特定主题或关键词,从而快速定位他们感兴趣的内容。 4. **如何使用Hacker News Browser-crx插件**: - 安装扩展:用户需要从浏览器的扩展商店下载并安装Hacker News Browser-crx插件。由于给定信息中提供的文件名为Hacker_News_Browser.crx,这表明这可能是一个Chrome扩展。 - 启用扩展:安装完成后,用户需要在浏览器的扩展管理界面启用该插件。 - 进行配置(可选):根据需要,用户可能需要对插件进行一些基本配置,比如设置Pocket账户等。 - 使用功能:安装并配置完成后,用户可以开始使用扩展提供的各项功能,包括浏览热门故事、保存内容到Pocket、发推文和搜索特定主题。 ### 总结: Hacker News Browser-crx插件作为一个扩展程序,为用户提供了便捷的途径去浏览和参与Hacker News上的讨论。该插件整合了多种功能,让读者能够实时关注科技动态,并与他人分享讨论。对于科技爱好者以及相关行业的专业人士,这是一个极具价值的工具,它能够帮助他们保持信息的及时更新和知识的持续吸收。
recommend-type

【MATLAB通信建模秘籍】:5个技巧让你为通信系统打下坚实基础

# 1. MATLAB通信建模基础概览 MATLAB(矩阵实验室)作为一种强大的科学计算软件,在通信系统建模和仿真领域扮演着重要角色。对于初学者而言,了解MATLAB的基础操作和通信建模的基本概念是开启深入学习之旅的第一步。 ## 1.1 MATLAB简介及其在通信领域的应用 MATLAB是一个高性能的数值计算和可视化软件,广泛应用于工程计算、算法开发、数据分析等领域。在通
recommend-type

汽车车载通讯技术

<think>我们正在讨论汽车车载通讯技术。根据之前的引用,我们知道汽车通信网络分为车内(有线)和车外(无线)两部分。同时,车载SOA架构使用多种通信技术(如CAN总线、以太网、MQTT等)来实现模块化和可扩展的通信。 用户的问题:汽车车载通讯技术原理及应用 回答结构: 1. 概述:简要介绍汽车车载通讯技术的定义和分类。 2. 原理部分:分别介绍车内网络和车外网络的主要技术原理。 2.1 车内网络:重点介绍CAN总线、LIN总线、FlexRay、MOST、Automotive Ethernet等。 2.2 车外网络:介绍V2X(包括V2V、V2I、V2P、V2N)及相
recommend-type

Dev Context Menu Utils (beta)-快速开发浏览器扩展

Dev Context Menu Utils (beta)-crx插件是一款面向开发者群体的浏览器扩展程序,其beta版本的命名暗示了它目前还在开发的早期阶段,可能尚未完全稳定或者未包含全部功能。从标题来看,这款扩展程序旨在为开发者提供便捷的上下文菜单功能。 上下文菜单(Context Menu)通常指的是当用户在软件或网页上右键点击时弹出的菜单。上下文菜单的内容根据点击的位置和对象会有所不同,它可以为用户提供快捷、针对当前情境的操作选项。在浏览器中,上下文菜单经常被用于快速访问开发者工具、页面操作、或是网页内容处理等功能。 标题中提到的“CNPJ”和“CPF”是巴西的法人和自然人的税务识别代码。CNPJ(Cadastro Nacional de Pessoas Jurídicas)是巴西所有公司和企业的全国性注册代码,而CPF(Cadastro de Pessoas Físicas)是巴西公民的个人税务识别码。在Dev Context Menu Utils (beta)中加入这两个菜单项,可能意味着插件能够让开发者在遇到需要验证或输入这些税务识别码的场景时,通过浏览器的右键菜单快速生成示例代码或进行其他相关操作。 “Lorem Ipsum”是设计和排版行业常用的一种占位文本,它起源于拉丁文学,经常用于设计软件的文本预览,以便设计师在不影响最终版式的情况下测试页面布局。在这款插件的上下文菜单中加入这一项,可能允许用户快速生成一段Lorem Ipsum文本,用于测试网页布局或者排版效果,从而让开发者在设计过程中获得更真实的视觉体验。 “电话”菜单项则可能用于提供快速生成或者验证电话号码格式的功能,这对于处理与电话相关的用户输入或数据录入工作非常有用。考虑到不同国家和地区的电话号码格式可能有所不同,这一功能可能允许用户选择特定的地区代码,从而生成相应格式的电话号码样例。 【标签】中提到的“扩展程序”一词,是指能够被浏览器安装并添加额外功能的软件。扩展程序可以改变浏览器的基本界面、行为和功能,是Web开发和测试中常见的工具。它们通常由浏览器的开发者或者第三方开发者创建,对于提升开发效率、调试和测试网站有着重要的作用。 【压缩包子文件的文件名称列表】中的“Dev_Context_Menu_Utils_(beta).crx”是这款插件的文件名,CRX是Chrome扩展程序的文件格式,类似于Windows平台上的.exe可执行文件格式,只不过CRX是为Chrome浏览器特别设计的。扩展程序开发者需要通过Chrome Web Store或者其他途径将CRX文件分发给用户,用户可以通过下载并安装CRX文件来使用扩展程序。 总体来说,Dev Context Menu Utils (beta) 插件通过为开发者提供便捷的上下文菜单项来增强开发效率,允许快速生成重要的代码片段和数据格式,从而简化开发过程中的常见任务。对于那些在开发阶段需要频繁使用这些功能的开发者来说,这款插件将是一个非常有价值的工具。然而,由于它目前还是beta版本,使用时可能需要注意可能存在的一些错误或功能限制,并建议开发者在实际项目中使用前先在测试环境中进行充分的测试。
recommend-type

【Coz进阶秘籍】:解锁工作流高级功能,提升效率的终极技巧

# 1. Coz工作流基础概述 工作流系统是现代IT运营不可或缺的一部分,它通过自动化执行任务和协调业务流程来提高效率。在本章节中,我们将首先介绍Coz工作流的基础概念和其在业务流程自动化中的应用。Coz工作流利用一系列预定义的任务、规则以及任务之间的依赖关系,按照既定规
recommend-type

HR和HRBP区别

<think>首先,用户意图是了解HR和HRBP的区别。用户问题是“HR和HRBP的区别”。我需要根据任务要求进行网络搜索。 任务要求: 1. 调用网络搜索工具进行检索。 2. 整合检索到的内容,生成详细且清晰的回答。 3. 禁止将所有引用内容集中在回答末尾。 4. 避免使用第一人称如“我”。 5. 避免步骤词汇如“首先”、“然后”。 6. 尽量将答案组织为多个方法或步骤,并在每个步骤间换行。 7. 对于代码和公式:如果有,必须使用指定格式。 8. 在回答最后添加相关问题部分,标题为“§§相关问题§§:”,后跟至少3个相关问题,每个用阿拉伯数字标号。 由于问题是关于HR和HRBP的区别,不
recommend-type

阻止Web加密货币挖掘的Miner Away扩展

### 知识点分析 #### 标题:“Miner Away-crx插件” **知识点**: 1. **CRX插件格式**:CRX是Chrome扩展程序的文件格式,它是一个ZIP压缩包,包含了扩展程序的所有文件和文件夹,例如HTML、JavaScript、CSS文件,以及扩展程序的清单文件(manifest.json)。CRX文件可以直接在Chrome浏览器的扩展管理界面拖拽安装。 2. **扩展程序(Extension)**:浏览器扩展程序是一种增加或改进浏览器功能的软件模块。它可以通过第三方开发者创建,用以提供特定的功能,比如用户界面定制、广告拦截、内容过滤等。 #### 描述:“在网上停止硬币矿工!” **知识点**: 3. **加密货币挖掘(Cryptocurrency Mining)**:指的是利用计算机的处理能力来计算加密货币的交易并维护区块链的过程。传统的加密货币挖掘需要大量的计算资源和电力消耗,近年来出现了基于Web的挖矿,即在网页中嵌入JavaScript代码,利用访问者的浏览器进行挖掘。 4. **矿工拒绝(Cryptominer Blocking)**:矿工拒绝功能的扩展通常用于识别和阻止这类JavaScript代码运行,从而保护用户设备的性能不受影响。这类扩展程序通常会维护一个黑名单,其中包含已知的挖矿脚本或网站地址。 5. **Opera Web Store**:Opera浏览器的官方扩展商店,类似于Chrome Web Store或Firefox Add-ons,是用户下载、安装和管理Opera浏览器扩展程序的平台。 6. **特征(Features)**: - **阻止JavaScript或Web矿工**:扩展能够检测并阻止网页加载的挖矿脚本。 - **域名选择性允许**:用户可以自行选择允许哪些特定网站加载JavaScript。 - **状态显示**:扩展程序会实时显示当前是否有挖矿行为发生。 - **通知功能**:当有网站尝试进行挖矿时,用户会即时收到桌面通知。 7. **技术实现细节**: - **黑名单机制**:扩展使用黑名单文件(*blacklist.txt*),其中包含被识别为执行挖矿行为的域名。 - **请求拦截**:对与黑名单中域名匹配的网站请求进行拦截,从而防止挖矿脚本运行。 #### 标签:“扩展程序” **知识点**: 8. **浏览器扩展程序的分类**:扩展程序通常根据其功能进行分类,如广告拦截器、密码管理器、下载管理器等。 9. **扩展程序的管理**:用户通常可以在浏览器的扩展管理界面中开启/关闭扩展、管理扩展权限、删除扩展等。 #### 压缩包子文件的文件名称列表:“Miner_Away.crx” **知识点**: 10. **文件命名约定**:扩展程序的文件名通常与其功能相关,例如本例中的“Miner_Away”暗示了该扩展用于阻止挖矿行为。 11. **文件的安装**:CRX文件可以通过多种方式进行安装,最常见的方式是直接从浏览器的扩展管理界面导入(通常通过拖拽文件到浏览器窗口),或者从扩展商店下载安装。 12. **文件的安全性**:用户应从可信来源下载扩展程序,避免下载可能含有恶意软件的非官方版本。 总结以上知识点,Miner Away扩展程序是一种专门设计用于阻止Web矿工的浏览器扩展,它通过黑名单机制拦截和阻止网站加载的加密货币挖掘脚本,保护用户的设备免受未经授权的资源消耗。该扩展还提供实时状态通知和请求拦截功能,为用户提供了更为安全和高效的网络浏览体验。
recommend-type

量子计算模拟与硬件发展:NISQ时代的探索

# 量子计算模拟与硬件发展:NISQ时代的探索 ## 1. 分布式计算在量子模拟中的应用 在量子计算领域,充分利用经典资源是一个重要的课题。分布式计算框架为我们提供了一种有效的途径,如今,这些框架在各大组织中广泛存在。例如,Apache Spark和Ray等分布式计算框架,已被广泛应用于大数据处理和深度学习模型训练。 量子计算模拟器也可以借助分布式生态系统,通过拆分需要执行的数学运算来实现。不过,将一个整体系统拆分为一组独立操作,需要我们深入理解当前问题的可拆分性。目前,处理分布式量子计算模拟的框架大多来自研究团队,像英特尔和百度等大型组织也有相关研究。 随着量子硬件的不断成熟,设备无关