利用Deepseek AI,在书上的例子增加了一些功能。QMap和QList可以替换为Java自带的HashMap和ArrayList。

QPlotter类

package jqt;

import io.qt.core.*;
import io.qt.gui.*;
import io.qt.widgets.*;
import java.util.concurrent.atomic.AtomicReference;

public class QPlotter extends QWidget {
    final int Margin = 50;
    QToolButton zoomInButton;
    QToolButton zoomOutButton;
    QMap<String, QList<QPointF>> curveMap = "QMap<String, QList<QPointF>>".create();
    QList<QPlotSettings> zoomStack = "QList<jqt.QPlotter\$QPlotSettings>".create();
    int curZoom;
    boolean rubberBandIsShown;
    QRect rubberBandRect = new QRect();
    QPixmap pixmap;

    // 新增悬停相关成员
    String highlightedCurve;
    QPointF hoverPoint;
    QLabel statusLabel;
    SpatialGrid spatialGrid;
    final double HOVER_THRESHOLD = 5.0;
    String xLabel = "";
    String yLabel = "";
    String title = "";
    boolean dataDirty = true;

    public QPlotter() {
        this(null);
    }

    public QPlotter(QWidget parent) {
        super(parent);
        setBackgroundRole(QPalette.ColorRole.Dark);
        setAutoFillBackground(true);
        setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding);
        setFocusPolicy(Qt.FocusPolicy.StrongFocus);
        rubberBandIsShown = false;

        // 初始化缩放按钮
        zoomInButton = new QToolButton(this);
        zoomInButton.setIcon(new QIcon(":/toolbar/zoomin.png"));
        zoomInButton.adjustSize();
        zoomInButton.clicked.connect(this::zoomIn);

        zoomOutButton = new QToolButton(this);
        zoomOutButton.setIcon(new QIcon(":/toolbar/zoomout.png"));
        zoomOutButton.adjustSize();
        zoomOutButton.clicked.connect(this::zoomOut);

        // 初始化状态标签
        statusLabel = new QLabel(this);
        statusLabel.setStyleSheet("background-color: rgba(255, 0, 0, 100); padding: 3px; border-radius: 3px;");
        statusLabel.hide();

        setMouseTracking(true);
        setPlotSettings(new QPlotSettings());
    }

    // 空间网格加速类
    private class SpatialGrid {
        private static final int CELL_SIZE = 50;
        private final QMap<QPair<Integer, Integer>, QList<QPointF>> grid = "QMap<QPair<Integer, Integer>, QList<QPointF>>".create();
        private QRectF dataRange;

        void build(QList<QPointF> points) {
            grid.clear();
            if (points.isEmpty()) return;

            dataRange = new QRectF(points.get(0).x(), points.get(0).y(), 0, 0);
            for (QPointF p : points) {
                int xCell = (int)(p.x() / CELL_SIZE);
                int yCell = (int)(p.y() / CELL_SIZE);
                QPair<Integer, Integer> key = new QPair<>(xCell, yCell);
                QList<QPointF> cell = grid.getOrDefault(key, new QList<>(QPointF.class));
                cell.add(p);
                grid.put(key, cell);
                dataRange = dataRange.united(new QRectF(p.x(), p.y(), 0, 0));
            }
        }

        QPointF findNearest(QPointF mousePos, double maxDist) {
            if (grid.isEmpty()) return null;

            int xCell = (int)(mousePos.x() / CELL_SIZE);
            int yCell = (int)(mousePos.y() / CELL_SIZE);
            AtomicReference<Double> minDistSq = new AtomicReference<>(maxDist * maxDist);
            AtomicReference<QPointF> nearest = new AtomicReference<>(null);

            for (int dx = -1; dx <= 1; dx++) {
                for (int dy = -1; dy <= 1; dy++) {
                    QPair<Integer, Integer> key = new QPair<>(xCell + dx, yCell + dy);
                    QList<QPointF> cell = grid.get(key);
                    if (cell != null) {
                        cell.forEach(p -> {
                            double dxVal = p.x() - mousePos.x();
                            double dyVal = p.y() - mousePos.y();
                            double distSq = dxVal * dxVal + dyVal * dyVal;
                            if (distSq < minDistSq.get()) {
                                minDistSq.set(distSq);
                                nearest.set(p);
                            }
                        });
                    }
                }
            }
            return nearest.get();
        }
    }

    public void setPlotSettings(QPlotSettings settings) {
        zoomStack.clear();
        zoomStack.append(settings);
        curZoom = 0;
        updateZoomButtons();
        refreshPixmap();
    }

    public void zoomIn() {
        if (curZoom < zoomStack.size() - 1) {
            curZoom++;
            updateZoomButtons();
            refreshPixmap();
        }
    }

    public void zoomOut() {
        if (curZoom > 0) {
            curZoom--;
            updateZoomButtons();
            refreshPixmap();
        }
    }

    private void updateZoomButtons() {
        zoomInButton.setEnabled(curZoom < zoomStack.size() - 1);
        zoomOutButton.setEnabled(curZoom > 0);
    }

    public void setCurveData(String id, QList<QPointF> data) {
        curveMap.put(id, data);
        dataDirty = true; // 标记数据变化
        refreshPixmap();
    }

    public void clearCurve(String id) {
        curveMap.remove(id);
        dataDirty = true; // 标记数据变化
        refreshPixmap();
    }

    // 新增方法:设置坐标轴标签
    public void setAxisLabels(String xLabel, String yLabel) {
        this.xLabel = xLabel;
        this.yLabel = yLabel;
        refreshPixmap();
    }

    // 新增方法:设置图表标题
    public void setTitle(String title) {
        this.title = title;
        refreshPixmap();
    }

    @Override
    public QSize minimumSizeHint() {
        return new QSize(6 * Margin, 4 * Margin);
    }

    @Override
    public QSize sizeHint() {
        return new QSize(12 * Margin, 8 * Margin);
    }

    @Override
    public void paintEvent(QPaintEvent e) {
        QStylePainter painter = new QStylePainter(this);
        painter.drawPixmap(0, 0, pixmap);

        if (rubberBandIsShown) {
            painter.setRenderHint(QPainter.RenderHint.Antialiasing);
            painter.setPen(new QColor(64, 64, 64, 200));
            painter.setBrush(new QColor(200, 200, 200, 50));
            painter.drawRect(rubberBandRect);
        }

        if (hasFocus()) {
            QStyleOptionFocusRect option = new QStyleOptionFocusRect();
            option.initFrom(this);
            option.setBackgroundColor(palette().dark().color());
            painter.drawPrimitive(QStyle.PrimitiveElement.PE_FrameFocusRect, option);
        }

        // 绘制高亮点
        if (hoverPoint != null) {
            painter.setRenderHint(QPainter.RenderHint.Antialiasing);
            QPoint pos = dataToWidget(hoverPoint);
            painter.setPen(Qt.GlobalColor.red);
            painter.setBrush(new QColor(255, 0, 0, 100));
            painter.drawEllipse(pos.x() - 5, pos.y() - 5, 10, 10);
        }

        painter.end();
    }

    @Override
    public void resizeEvent(QResizeEvent e) {
        int x = width() - (zoomInButton.width() + zoomOutButton.width() + 10);
        zoomInButton.move(x, 5);
        zoomOutButton.move(x + zoomInButton.width() + 5, 5);
        refreshPixmap();
    }

    @Override
    public void mousePressEvent(QMouseEvent e) {
        QRect plotArea = new QRect(Margin, Margin, width() - 2 * Margin, height() - 2 * Margin);
        if (e.button() == Qt.MouseButton.LeftButton && plotArea.contains(e.pos())) {
            rubberBandIsShown = true;
            rubberBandRect.setTopLeft(e.pos());
            rubberBandRect.setBottomRight(e.pos());
            updateRubberBandRegion();
            setCursor(Qt.CursorShape.CrossCursor);
        }
    }

    @Override
    public void mouseMoveEvent(QMouseEvent e) {
        // 处理橡皮筋
        if (rubberBandIsShown) {
            QRect plotArea = new QRect(Margin, Margin, width() - 2 * Margin, height() - 2 * Margin);
            int x = Math.min(Math.max(e.pos().x(), plotArea.left()), plotArea.right());
            int y = Math.min(Math.max(e.pos().y(), plotArea.top()), plotArea.bottom());

            updateRubberBandRegion();
            rubberBandRect.setBottomRight(new QPoint(x, y));
            updateRubberBandRegion();
        }

        // 处理悬停
        QPointF dataPos = widgetToData(e.pos());
        findHoverPoint(dataPos, e.pos());
        updateHoverDisplay(e.globalPos());
        update();
    }

    private QPointF widgetToData(QPoint widgetPos) {
        QPlotSettings settings = zoomStack.get(curZoom);
        QRect plotArea = new QRect(Margin, Margin, width() - 2 * Margin, height() - 2 * Margin);
        double x = settings.minX + (widgetPos.x() - plotArea.x()) * settings.spanX() / plotArea.width();
        double y = settings.maxY - (widgetPos.y() - plotArea.y()) * settings.spanY() / plotArea.height();
        return new QPointF(x, y);
    }

    private QPoint dataToWidget(QPointF dataPoint) {
        QPlotSettings settings = zoomStack.get(curZoom);
        QRect plotArea = new QRect(Margin, Margin, width() - 2 * Margin, height() - 2 * Margin);
        int x = Margin + (int)((dataPoint.x() - settings.minX) * plotArea.width() / settings.spanX());
        int y = Margin + (int)((settings.maxY - dataPoint.y()) * plotArea.height() / settings.spanY());
        return new QPoint(x, y);
    }

    private void findHoverPoint(QPointF dataPos, QPoint widgetPos) {
        highlightedCurve = null;
        hoverPoint = null;

        for (String curveId : curveMap.keySet()) {
            QList<QPointF> points = curveMap.get(curveId);
            if (points.isEmpty()) continue;

            if (spatialGrid == null || dataDirty) {
                spatialGrid = new SpatialGrid();
                spatialGrid.build(points);
                dataDirty = false;
            } else if (!spatialGrid.dataRange.contains(dataPos)){
                spatialGrid.build(points);
            }

            QPointF nearest = spatialGrid.findNearest(dataPos, HOVER_THRESHOLD);
            if (nearest != null) {
                QPoint nearestWidgetPos = dataToWidget(nearest);
                // 手动计算两点距离
                double dx = nearestWidgetPos.x() - widgetPos.x();
                double dy = nearestWidgetPos.y() - widgetPos.y();
                double distance = Math.sqrt(dx*dx + dy*dy);
                if (distance <= HOVER_THRESHOLD) {
                    highlightedCurve = curveId;
                    hoverPoint = nearest;
                    break;
                }
            }
        }
    }

    private void updateHoverDisplay(QPoint globalPos) {
        if (hoverPoint != null) {
            statusLabel.setText(String.format("(%.3f, %.3f)", hoverPoint.x(), hoverPoint.y()));
            statusLabel.adjustSize();
            QPoint localPos = mapFromGlobal(globalPos).add(new QPoint(15, -20));
            statusLabel.move(localPos);
            statusLabel.show();
        } else {
            statusLabel.hide();
        }
    }

    @Override
    public void mouseReleaseEvent(QMouseEvent e) {
        if (e.button() == Qt.MouseButton.LeftButton && rubberBandIsShown) {
            rubberBandIsShown = false;
            updateRubberBandRegion();
            unsetCursor();

            QRect rect = rubberBandRect.normalized();
            if (rect.width() < 4 || rect.height() < 4) return;

            QRect plotArea = new QRect(Margin, Margin, width() - 2 * Margin, height() - 2 * Margin);
            QPlotSettings prevSettings = zoomStack.get(curZoom);

            double xRatio = prevSettings.spanX() / plotArea.width();
            double yRatio = prevSettings.spanY() / plotArea.height();

            QPlotSettings settings = new QPlotSettings();
            settings.minX = prevSettings.minX + xRatio * (rect.left() - plotArea.left());
            settings.maxX = prevSettings.minX + xRatio * (rect.right() - plotArea.left());
            settings.minY = prevSettings.maxY - yRatio * (rect.bottom() - plotArea.top());
            settings.maxY = prevSettings.maxY - yRatio * (rect.top() - plotArea.top());

            if (settings.spanX() <= 0 || settings.spanY() <= 0) return;

            settings.adjust();

            if (curZoom < zoomStack.size() - 1) {
                zoomStack = new QList<>(zoomStack.mid(0, curZoom + 1));
            }
            zoomStack.add(settings);
            curZoom = zoomStack.size() - 1;
            updateZoomButtons();
            refreshPixmap();
        }
    }

    @Override
    public void leaveEvent(QEvent e) {
        highlightedCurve = null;
        hoverPoint = null;
        statusLabel.hide();
        update();
    }

    @Override
    public void keyPressEvent(QKeyEvent e) {
        switch (Qt.Key.resolve(e.key())) {
            case Key_Plus: zoomIn(); break;
            case Key_Minus: zoomOut(); break;
            case Key_Left: zoomStack.get(curZoom).scroll(-1, 0); break;
            case Key_Right: zoomStack.get(curZoom).scroll(+1, 0); break;
            case Key_Down: zoomStack.get(curZoom).scroll(0, -1); break;
            case Key_Up: zoomStack.get(curZoom).scroll(0, +1); break;
            default: super.keyPressEvent(e);
        }
        refreshPixmap();
    }

    private void updateRubberBandRegion() {
        update(rubberBandRect.normalized().adjusted(-1, -1, 1, 1));
    }

    private void refreshPixmap() {
        if (pixmap != null) {
            pixmap.dispose(); // 显式释放资源
        }
        pixmap = new QPixmap(size());
        pixmap.fill(palette().color(QPalette.ColorRole.Window));

        QPainter painter = new QPainter(pixmap);
        try {
            painter.setRenderHint(QPainter.RenderHint.Antialiasing);
            drawGrid(painter);
            drawCurves(painter);
            drawLegend(painter);
        } finally {
            painter.end();
        }
        update();
    }

    private void drawGrid(QPainter painter) {
        QRect rect = new QRect(Margin, Margin, width() - 2 * Margin, height() - 2 * Margin);
        if (!rect.isValid()) return;

        QPlotSettings settings = zoomStack.get(curZoom);

        QFont labelFont = new QFont(font());
        labelFont.setBold(true);
        QFontMetrics fm = painter.fontMetrics();

        // ================ 绘制标题 ================
        if (!title.isEmpty()) {
            painter.setFont(labelFont);
            // 标题位置上移,距顶部10像素
            painter.drawText(0, 10, width(), 30,
                    Qt.AlignmentFlag.AlignCenter.value(), title);
        }

        // ================ 绘制X轴标签 ================
        if (!xLabel.isEmpty()) {
            painter.setFont(labelFont);
            // 位于X轴左端下方(图左下角)
            painter.drawText(width() - Margin, height() - Margin / 2,
                    width() - 2*Margin, Margin,
                    Qt.AlignmentFlag.AlignLeft.value(), xLabel);
        }

        // ================ 绘制Y轴标签 ================
        if (!yLabel.isEmpty()) {
            painter.setFont(labelFont);
            // 垂直排列在Y轴下端左侧
            int startX = 5;
            int startY = Margin / 2;
            int charHeight = fm.height();

            // 将字符串转换为字符数组
            char[] chars = yLabel.toCharArray();
            for (int i = 0; i < chars.length; i++) {
                String ch = String.valueOf(chars[i]);
                // 每个字符单独绘制
                painter.drawText(startX, startY + i*charHeight, ch);
            }
        }

        QPen axisPen = new QPen(palette().color(QPalette.ColorRole.WindowText), 1);
        painter.setFont(font());

        // X轴
        for (int i = 0; i <= settings.numXTicks; i++) {
            double value = settings.minX + (i * settings.spanX() / settings.numXTicks);
            int x = rect.left() + (int)((value - settings.minX) * rect.width() / settings.spanX());

            painter.setPen(axisPen);
            painter.drawLine(x, rect.top(), x, rect.bottom());

            String text = String.format("%.2f", value);
            painter.drawText(x - 50, rect.bottom() + 5, 100, 20,
                    Qt.AlignmentFlag.AlignHCenter.value() | Qt.AlignmentFlag.AlignTop.value(),
                    text);
        }

        // Y轴
        for (int j = 0; j <= settings.numYTicks; j++) {
            double value = settings.minY + (j * settings.spanY() / settings.numYTicks);
            int y = rect.bottom() - (int)((value - settings.minY) * rect.height() / settings.spanY());

            painter.setPen(axisPen);
            painter.drawLine(rect.left(), y, rect.right(), y);

            String text = String.format("%.2f", value);
            painter.drawText(rect.left() - Margin, y - 10, Margin - 5, 20,
                    Qt.AlignmentFlag.AlignRight.value() | Qt.AlignmentFlag.AlignVCenter.value(),
                    text);
        }

        painter.drawRect(rect.adjusted(0, 0, -1, -1));
    }

    private void drawCurves(QPainter painter) {
        QPlotSettings settings = zoomStack.get(curZoom);
        QRect rect = new QRect(Margin, Margin, width() - 2 * Margin, height() - 2 * Margin);
        if (!rect.isValid()) return;

        painter.setClipRect(rect.adjusted(+1, +1, -1, -1));
        int colorId = 0;

        for (String id : curveMap.keySet()) {
            QList<QPointF> data = curveMap.get(id);
            QPolygonF polyline = new QPolygonF();

            for (QPointF point : data) {
                double x = rect.left() + ((point.x() - settings.minX) * rect.width() / settings.spanX());
                double y = rect.bottom() - ((point.y() - settings.minY) * rect.height() / settings.spanY());
                polyline.add(new QPointF(x, y));
            }

            boolean isHighlighted = id.equals(highlightedCurve);
            float hue = (colorId * 0.618f) % 1.0f;
            QColor color = QColor.fromHsvF(hue, 0.8f, 0.8f);
            color.setAlphaF(isHighlighted ? 1.0f : 0.6f);

            painter.setPen(new QPen(color, isHighlighted ? 3 : 1));
            painter.drawPolyline(polyline);
            colorId++;
        }
    }

    private void drawLegend(QPainter painter) {
        int legendX = width() - 150;
        int legendY = Margin + 20;
        int colorId = 0;

        painter.setFont(font());
        painter.setPen(palette().color(QPalette.ColorRole.WindowText));

        for (String id : curveMap.keySet()) {
            float hue = (colorId * 0.618f) % 1.0f;
            QColor color = QColor.fromHsvF(hue, 0.8f, 0.8f);

            painter.fillRect(legendX, legendY - 10, 15, 15, color);
            painter.drawText(legendX + 20, legendY + 5, id);

            legendY += 20;
            colorId++;
        }
    }

    public void autoScale() {
        if (curveMap.isEmpty()) {
            setPlotSettings(new QPlotSettings()); // 设置默认范围
            return;
        }

        double minX = Double.MAX_VALUE;
        double maxX = -Double.MAX_VALUE;
        double minY = Double.MAX_VALUE;
        double maxY = -Double.MAX_VALUE;

        for (QList<QPointF> data : curveMap.values()) {
            for (QPointF point : data) {
                minX = Math.min(minX, point.x());
                maxX = Math.max(maxX, point.x());
                minY = Math.min(minY, point.y());
                maxY = Math.max(maxY, point.y());
            }
        }

        if (minX == maxX) {
            minX -= 1.0;
            maxX += 1.0;
        }
        if (minY == maxY) {
            minY -= 1.0;
            maxY += 1.0;
        }

        double xMargin = 0.1 * (maxX - minX);
        double yMargin = 0.1 * (maxY - minY);

        QPlotSettings settings = new QPlotSettings();
        settings.minX = minX - xMargin;
        settings.maxX = maxX + xMargin;
        settings.minY = minY - yMargin;
        settings.maxY = maxY + yMargin;

        settings.adjust();
        setPlotSettings(settings);
    }

    public static class QPlotSettings {
        public double minX;
        public double maxX;
        public int numXTicks;
        public double minY;
        public double maxY;
        public int numYTicks;

        public QPlotSettings() {
            minX = 0.0;
            maxX = 10.0;
            numXTicks = 5;
            minY = 0.0;
            maxY = 10.0;
            numYTicks = 5;
        }

        public double spanX() { return maxX - minX; }
        public double spanY() { return maxY - minY; }

        public void scroll(int dx, int dy) {
            double stepX = spanX() / numXTicks;
            minX += dx * stepX;
            maxX += dx * stepX;

            double stepY = spanY() / numYTicks;
            minY += dy * stepY;
            maxY += dy * stepY;
        }

        public void adjust() {
            adjustAxis(true);
            adjustAxis(false);
        }

        private void adjustAxis(boolean isXAxis) {
            double min = isXAxis ? minX : minY;
            double max = isXAxis ? maxX : maxY;
            final int MinTicks = 4;

            if (Math.abs(max - min) < 1e-6) {
                max = min + (min >= 0 ? 1e-6 : -1e-6);
            }

            double range = max - min;
            double grossStep = range / MinTicks;
            double step = calculateOptimalStep(grossStep);

            double newMin = Math.floor(min/step)*step;
            double newMax = Math.ceil(max/step)*step;
            int numTicks = (int) Math.round((newMax - newMin)/step);

            while(numTicks < MinTicks) {
                step /= 2.0;
                newMin = Math.floor(min/step)*step;
                newMax = Math.ceil(max/step)*step;
                numTicks = (int) Math.round((newMax - newMin)/step);
            }

            if (isXAxis) {
                this.minX = newMin;
                this.maxX = newMax;
                this.numXTicks = numTicks;
            } else {
                this.minY = newMin;
                this.maxY = newMax;
                this.numYTicks = numTicks;
            }
        }

        private double calculateOptimalStep(double grossStep) {
            double absStep = Math.abs(grossStep);
            double exponent = Math.floor(Math.log10(absStep));
            double fraction = absStep / Math.pow(10, exponent);

            double[] candidates = {1.0, 2.0, 5.0, 10.0};
            double optimal = 10.0;
            for (double candidate : candidates) {
                if (candidate >= fraction) {
                    optimal = candidate;
                    break;
                }
            }

            double step = optimal * Math.pow(10, exponent);
            return grossStep < 0 ? -step : step;
        }
    }
}

测试代码

package one;

import io.qt.core.QList;
import io.qt.core.QPointF;
import jqt.Jqt;
import jqt.QPlotter;

public class Main{

    public static void main(String[] args){
        Jqt qt = new Jqt(args, null);
        QPlotter plotter = new QPlotter();
        // 测试数据1:正负线性数据
        QList<QPointF> linearData = new QList<>(QPointF.class);
        for (int i = -10; i <= 10; i++) {
            linearData.add(new QPointF(i, i));
        }

        // 测试数据2:完整正弦波
        QList<QPointF> sineData = new QList<>(QPointF.class);
        for (int i = 0; i <= 100; i++) {
            double x = i * 0.2;
            sineData.add(new QPointF(x, 10 * Math.sin(x)));
        }
        plotter.setTitle("Plotter");
        plotter.setAxisLabels("x轴", "y轴");
        plotter.setCurveData("Linear", linearData);
        plotter.setCurveData("Sine Wave", sineData);
        plotter.autoScale();
        plotter.show();
        qt.run();
    }

}

运行效果图

 

posted on 2025-04-19 13:34  dalgleish  阅读(24)  评论(0)    收藏  举报