Über... hinzugefügt, Icon hinzugefügt
This commit is contained in:
260
src/videowidget.cpp
Normal file
260
src/videowidget.cpp
Normal file
@@ -0,0 +1,260 @@
|
||||
#include "videowidget.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QFileDialog>
|
||||
#include <QDir>
|
||||
#include <QDateTime>
|
||||
#include <QStandardPaths>
|
||||
#include <QMetaObject>
|
||||
#include <QResizeEvent>
|
||||
|
||||
// Enumerate /dev/video* entries
|
||||
static QStringList enumerateVideoDevices()
|
||||
{
|
||||
QStringList devices;
|
||||
const QDir dev("/dev");
|
||||
const auto entries = dev.entryList({"video*"}, QDir::System, QDir::Name);
|
||||
for (const auto &e : entries)
|
||||
devices << "/dev/" + e;
|
||||
return devices;
|
||||
}
|
||||
|
||||
// ── ctor / dtor ───────────────────────────────────────────────────────────────
|
||||
|
||||
VideoWidget::VideoWidget(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
setupUi();
|
||||
|
||||
m_thread = new QThread(this);
|
||||
m_worker = new V4l2Worker();
|
||||
m_worker->moveToThread(m_thread);
|
||||
|
||||
connect(m_thread, &QThread::finished, m_worker, &QObject::deleteLater);
|
||||
|
||||
connect(m_worker, &V4l2Worker::newFrame, this, &VideoWidget::onNewFrame,
|
||||
Qt::QueuedConnection);
|
||||
connect(m_worker, &V4l2Worker::captureStarted, this, [this](const QString &dev, int w, int h, const QString &fmt) {
|
||||
m_infoLabel->setText(tr("%1 %2×%3 %4").arg(dev).arg(w).arg(h).arg(fmt));
|
||||
m_running = true;
|
||||
m_startBtn->setText(tr("■ Stop"));
|
||||
});
|
||||
connect(m_worker, &V4l2Worker::captureStopped, this, [this] {
|
||||
m_running = false;
|
||||
m_startBtn->setText(tr("▶ Start"));
|
||||
m_infoLabel->setText(tr("Stopped"));
|
||||
update();
|
||||
});
|
||||
connect(m_worker, &V4l2Worker::errorOccurred, this, [this](const QString &msg) {
|
||||
m_infoLabel->setText(tr("Error: %1").arg(msg));
|
||||
m_running = false;
|
||||
m_startBtn->setText(tr("▶ Start"));
|
||||
});
|
||||
|
||||
m_thread->start();
|
||||
}
|
||||
|
||||
void VideoWidget::shutdown()
|
||||
{
|
||||
if (m_shutdownDone)
|
||||
return;
|
||||
m_shutdownDone = true;
|
||||
|
||||
// stopCapture() sets m_running=false and joins the std::thread internally.
|
||||
// Must happen before we quit the Qt event thread.
|
||||
m_worker->stopCapture();
|
||||
|
||||
if (m_thread && m_thread->isRunning()) {
|
||||
m_thread->quit();
|
||||
m_thread->wait(5000);
|
||||
if (m_thread->isRunning())
|
||||
m_thread->terminate();
|
||||
}
|
||||
}
|
||||
|
||||
VideoWidget::~VideoWidget()
|
||||
{
|
||||
shutdown();
|
||||
}
|
||||
|
||||
// ── UI setup ─────────────────────────────────────────────────────────────────
|
||||
|
||||
void VideoWidget::setupUi()
|
||||
{
|
||||
setMinimumHeight(140);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(2, 2, 2, 2);
|
||||
layout->setSpacing(3);
|
||||
|
||||
// ── Controls row ──────────────────────────────────────────────────────────
|
||||
auto *ctrlRow = new QHBoxLayout();
|
||||
ctrlRow->setSpacing(4);
|
||||
|
||||
m_deviceCombo = new QComboBox(this);
|
||||
m_deviceCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
refreshDeviceList();
|
||||
|
||||
auto *refreshBtn = new QPushButton("↻", this);
|
||||
refreshBtn->setMaximumWidth(26);
|
||||
refreshBtn->setToolTip(tr("Refresh device list"));
|
||||
connect(refreshBtn, &QPushButton::clicked, this, &VideoWidget::refreshDeviceList);
|
||||
|
||||
m_startBtn = new QPushButton(tr("▶ Start"), this);
|
||||
m_startBtn->setMaximumWidth(70);
|
||||
connect(m_startBtn, &QPushButton::clicked, this, &VideoWidget::onStartStop);
|
||||
|
||||
m_freezeBtn = new QPushButton(tr("❚❚ Freeze"), this);
|
||||
m_freezeBtn->setCheckable(true);
|
||||
m_freezeBtn->setMaximumWidth(80);
|
||||
connect(m_freezeBtn, &QPushButton::toggled, this, &VideoWidget::onFreeze);
|
||||
|
||||
m_screenshotBtn = new QPushButton(tr("📷"), this);
|
||||
m_screenshotBtn->setMaximumWidth(32);
|
||||
m_screenshotBtn->setToolTip(tr("Save screenshot (PNG/JPG)"));
|
||||
connect(m_screenshotBtn, &QPushButton::clicked, this, &VideoWidget::onScreenshot);
|
||||
|
||||
ctrlRow->addWidget(m_deviceCombo, 1);
|
||||
ctrlRow->addWidget(refreshBtn);
|
||||
ctrlRow->addWidget(m_startBtn);
|
||||
ctrlRow->addWidget(m_freezeBtn);
|
||||
ctrlRow->addWidget(m_screenshotBtn);
|
||||
layout->addLayout(ctrlRow);
|
||||
|
||||
// ── Info label ────────────────────────────────────────────────────────────
|
||||
m_infoLabel = new QLabel(tr("No device started"), this);
|
||||
m_infoLabel->setStyleSheet("color: gray; font-size: 9px;");
|
||||
layout->addWidget(m_infoLabel);
|
||||
|
||||
// The video frame is drawn directly in paintEvent – no QLabel needed.
|
||||
// We leave stretch space for it.
|
||||
layout->addStretch(1);
|
||||
}
|
||||
|
||||
// ── Slots ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
void VideoWidget::onStartStop()
|
||||
{
|
||||
if (m_running)
|
||||
stopCapture();
|
||||
else
|
||||
startCapture();
|
||||
}
|
||||
|
||||
void VideoWidget::onFreeze(bool frozen)
|
||||
{
|
||||
m_frozen = frozen;
|
||||
m_freezeBtn->setText(frozen ? tr("▶ Live") : tr("❚❚ Freeze"));
|
||||
}
|
||||
|
||||
void VideoWidget::onScreenshot()
|
||||
{
|
||||
if (m_frame.isNull())
|
||||
return;
|
||||
|
||||
const QString defaultName =
|
||||
QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)
|
||||
+ "/uartscope_"
|
||||
+ QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss")
|
||||
+ ".png";
|
||||
|
||||
const QString path = QFileDialog::getSaveFileName(
|
||||
this, tr("Save screenshot"), defaultName,
|
||||
tr("PNG Image (*.png);;JPEG Image (*.jpg *.jpeg)"));
|
||||
|
||||
if (!path.isEmpty())
|
||||
m_frame.save(path);
|
||||
}
|
||||
|
||||
void VideoWidget::onNewFrame(const QImage &frame)
|
||||
{
|
||||
if (m_frozen)
|
||||
return;
|
||||
m_frame = frame;
|
||||
updateScaled();
|
||||
update(); // trigger paintEvent
|
||||
}
|
||||
|
||||
void VideoWidget::refreshDeviceList()
|
||||
{
|
||||
const QString current = m_deviceCombo->currentText();
|
||||
m_deviceCombo->clear();
|
||||
const auto devices = enumerateVideoDevices();
|
||||
for (const auto &d : devices)
|
||||
m_deviceCombo->addItem(d);
|
||||
// Restore previous selection if still present
|
||||
const int idx = m_deviceCombo->findText(current);
|
||||
if (idx >= 0)
|
||||
m_deviceCombo->setCurrentIndex(idx);
|
||||
}
|
||||
|
||||
// ── Capture start/stop ────────────────────────────────────────────────────────
|
||||
|
||||
void VideoWidget::startCapture()
|
||||
{
|
||||
const QString dev = m_deviceCombo->currentText();
|
||||
if (dev.isEmpty())
|
||||
return;
|
||||
m_frame = QImage();
|
||||
m_scaled = QImage();
|
||||
// startCapture blocks in the worker thread until stopCapture() is called
|
||||
QMetaObject::invokeMethod(m_worker, "startCapture",
|
||||
Qt::QueuedConnection,
|
||||
Q_ARG(QString, dev),
|
||||
Q_ARG(int, 1920),
|
||||
Q_ARG(int, 1080));
|
||||
}
|
||||
|
||||
void VideoWidget::stopCapture()
|
||||
{
|
||||
QMetaObject::invokeMethod(m_worker, "stopCapture", Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
// ── Paint ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
void VideoWidget::updateScaled()
|
||||
{
|
||||
// Video area = widget height minus the two control rows (~55 px)
|
||||
const QRect videoRect(0, 55, width(), height() - 55);
|
||||
if (videoRect.width() < 10 || videoRect.height() < 10 || m_frame.isNull())
|
||||
return;
|
||||
|
||||
m_scaled = m_frame.scaled(videoRect.size(), Qt::KeepAspectRatio,
|
||||
Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
void VideoWidget::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter p(this);
|
||||
|
||||
// Background
|
||||
p.fillRect(rect(), QColor(0x10, 0x10, 0x10));
|
||||
|
||||
if (m_scaled.isNull()) {
|
||||
p.setPen(QColor(0x44, 0x44, 0x44));
|
||||
p.drawText(rect(), Qt::AlignCenter,
|
||||
m_running ? tr("Waiting for frames…") : tr("No signal"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Centre the scaled frame in the video area
|
||||
const QRect videoRect(0, 55, width(), height() - 55);
|
||||
const QPoint origin(
|
||||
videoRect.left() + (videoRect.width() - m_scaled.width()) / 2,
|
||||
videoRect.top() + (videoRect.height() - m_scaled.height()) / 2);
|
||||
p.drawImage(origin, m_scaled);
|
||||
|
||||
// Frozen overlay
|
||||
if (m_frozen) {
|
||||
p.setPen(QColor(0xff, 0xcc, 0x00));
|
||||
p.setFont(QFont("Sans", 9, QFont::Bold));
|
||||
p.drawText(videoRect.adjusted(4, 4, -4, -4),
|
||||
Qt::AlignTop | Qt::AlignRight, tr("FREEZE"));
|
||||
}
|
||||
}
|
||||
|
||||
void VideoWidget::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
updateScaled();
|
||||
}
|
||||
Reference in New Issue
Block a user