Ejemplo de escena de diagrama
Demuestra cómo utilizar el framework Vista Gráfica.

El ejemplo Diagram Scene es una aplicación en la que se puede crear un diagrama de flujo. Es posible añadir formas de diagrama de flujo y texto y conectar las formas mediante flechas como se muestra en la imagen de arriba. Las formas, las flechas y el texto pueden tener diferentes colores, y es posible cambiar la fuente, el estilo y el subrayado del texto.
El framework de vistas gráficas de Qt está diseñado para gestionar y mostrar elementos gráficos 2D personalizados. Las clases principales del marco son QGraphicsItem, QGraphicsScene y QGraphicsView. La escena gráfica gestiona los elementos y proporciona una superficie para ellos. QGraphicsView es un widget que se utiliza para representar una escena en la pantalla. Para una descripción más detallada del framework, véase Graphics View Framework.
En este ejemplo mostramos cómo crear estas escenas gráficas y elementos personalizados implementando clases que heredan de QGraphicsScene y QGraphicsItem.
En particular mostramos cómo:
- Crear elementos gráficos personalizados.
- Manejar eventos de ratón y movimiento de elementos.
- Implementar una escena gráfica que pueda gestionar nuestros elementos personalizados.
- Pintar elementos personalizados.
- Crear un elemento de texto movible y editable.
El ejemplo consta de las siguientes clases:
MainWindowcrea los widgets y los muestra en un QMainWindow. También gestiona la interacción entre los widgets y la escena gráfica, la vista y los ítems.DiagramItemhereda de QGraphicsPolygonItem y representa una forma de diagrama de flujo.TextDiagramItemhereda de QGraphicsTextItem y representa elementos de texto en el diagrama. La clase añade soporte para mover el elemento con el ratón, que no está soportado por QGraphicsTextItem.ArrowHereda de QGraphicsLineItem y es una flecha que conecta dos DiagramItems.DiagramScenehereda QGraphicsDiagramScene y provee soporte paraDiagramItem,ArrowyDiagramTextItem(Además del soporte ya manejado por QGraphicsScene).
Definición de la Clase MainWindow
class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(); private slots: void backgroundButtonGroupClicked(QAbstractButton *button); void buttonGroupClicked(QAbstractButton *button); void deleteItem(); void pointerGroupClicked(); void bringToFront(); void sendToBack(); void itemInserted(DiagramItem *item); void textInserted(QGraphicsTextItem *item); void currentFontChanged(const QFont &font); void fontSizeChanged(const QString &size); void sceneScaleChanged(const QString &scale); void textColorChanged(); void itemColorChanged(); void lineColorChanged(); void textButtonTriggered(); void fillButtonTriggered(); void lineButtonTriggered(); void handleFontChange(); void itemSelected(QGraphicsItem *item); void about(); private: void createToolBox(); void createActions(); void createMenus(); void createToolbars(); QWidget *createBackgroundCellWidget(const QString &text, const QString &image); QWidget *createCellWidget(const QString &text, DiagramItem::DiagramType type); template<typename PointerToMemberFunction> QMenu *createColorMenu(const PointerToMemberFunction &slot, QColor defaultColor); QIcon createColorToolButtonIcon(const QString &image, QColor color); QIcon createColorIcon(QColor color); DiagramScene *scene; QGraphicsView *view; QAction *exitAction; QAction *addAction; QAction *deleteAction; QAction *toFrontAction; QAction *sendBackAction; QAction *aboutAction; QMenu *fileMenu; QMenu *itemMenu; QMenu *aboutMenu; QToolBar *textToolBar; QToolBar *editToolBar; QToolBar *colorToolBar; QToolBar *pointerToolbar; QComboBox *sceneScaleCombo; QComboBox *itemColorCombo; QComboBox *textColorCombo; QComboBox *fontSizeCombo; QFontComboBox *fontCombo; QToolBox *toolBox; QButtonGroup *buttonGroup; QButtonGroup *pointerTypeGroup; QButtonGroup *backgroundButtonGroup; QToolButton *fontColorToolButton; QToolButton *fillColorToolButton; QToolButton *lineColorToolButton; QAction *boldAction; QAction *underlineAction; QAction *italicAction; QAction *textAction; QAction *fillAction; QAction *lineAction; };
La clase MainWindow crea y dispone los widgets en un QMainWindow. La clase reenvía la entrada de los widgets al DiagramScene. También actualiza sus widgets cuando el elemento de texto de la escena de diagrama cambia, o un elemento de diagrama o un elemento de texto de diagrama se inserta en la escena.
La clase también elimina elementos de la escena y maneja el orden z, que decide el orden en que los elementos se dibujan cuando se superponen entre sí.
Implementación de la clase MainWindow
Comenzamos con un vistazo al constructor:
MainWindow::MainWindow() { createActions(); createToolBox(); createMenus(); scene = new DiagramScene(itemMenu, this); scene->setSceneRect(QRectF(0, 0, 5000, 5000)); connect(scene, &DiagramScene::itemInserted, this, &MainWindow::itemInserted); connect(scene, &DiagramScene::textInserted, this, &MainWindow::textInserted); connect(scene, &DiagramScene::itemSelected, this, &MainWindow::itemSelected); createToolbars(); QHBoxLayout *layout = new QHBoxLayout; layout->addWidget(toolBox); view = new QGraphicsView(scene); layout->addWidget(view); QWidget *widget = new QWidget; widget->setLayout(layout); setCentralWidget(widget); setWindowTitle(tr("Diagramscene")); setUnifiedTitleAndToolBarOnMac(true); }
En el constructor llamamos a métodos para crear los widgets y diseños del ejemplo antes de crear la escena del diagrama. Las barras de herramientas deben ser creadas después de la escena ya que se conectan a sus señales. A continuación, colocamos los widgets en la ventana.
Nos conectamos a las ranuras itemInserted() y textInserted() de las escenas de diagrama ya que queremos desmarcar los botones de la caja de herramientas cuando se inserte un elemento. Cuando se selecciona un elemento en la escena, recibimos la señal itemSelected(). La utilizamos para actualizar los widgets que muestran las propiedades de la fuente si el elemento seleccionado es un DiagramTextItem.
La función createToolBox() crea y despliega los widgets del toolBox QToolBox . No la examinaremos con un alto nivel de detalle ya que no se ocupa de la funcionalidad específica del marco gráfico. He aquí su implementación:
void MainWindow::createToolBox() { buttonGroup = new QButtonGroup(this); buttonGroup->setExclusive(false); connect(buttonGroup, QOverload<QAbstractButton *>::of(&QButtonGroup::buttonClicked), this, &MainWindow::buttonGroupClicked); QGridLayout *layout = new QGridLayout; layout->addWidget(createCellWidget(tr("Conditional"), DiagramItem::Conditional), 0, 0); layout->addWidget(createCellWidget(tr("Process"), DiagramItem::Step),0, 1); layout->addWidget(createCellWidget(tr("Input/Output"), DiagramItem::Io), 1, 0);
Esta parte de la función configura el elemento widget con pestañas que contiene las formas del diagrama de flujo. Un QButtonGroup exclusivo siempre mantiene un botón marcado; queremos que el grupo permita que todos los botones estén desmarcados. Seguimos utilizando un grupo de botones ya que podemos asociar datos de usuario, que utilizamos para almacenar el tipo de diagrama, con cada botón. La función createCellWidget() configura los botones en el elemento del widget con pestañas y se examina más adelante.
Los botones del elemento widget con pestañas de fondo se configuran de la misma manera, así que pasamos a la creación de la caja de herramientas:
toolBox = new QToolBox; toolBox->setSizePolicy(QSizePolicy(QSizePolicy::Maximum, QSizePolicy::Ignored)); toolBox->setMinimumWidth(itemWidget->sizeHint().width()); toolBox->addItem(itemWidget, tr("Basic Flowchart Shapes")); toolBox->addItem(backgroundWidget, tr("Backgrounds")); }
Establecemos el tamaño preferido de la caja de herramientas como máximo. De esta manera, se da más espacio a la vista gráfica.
Aquí está la función createActions():
void MainWindow::createActions() { toFrontAction = new QAction(QIcon(":/images/bringtofront.png"), tr("Bring to &Front"), this); toFrontAction->setShortcut(tr("Ctrl+F")); toFrontAction->setStatusTip(tr("Bring item to front")); connect(toFrontAction, &QAction::triggered, this, &MainWindow::bringToFront);
Mostramos un ejemplo de la creación de una acción. La funcionalidad que activan las acciones se discute en las ranuras a las que conectamos las acciones.
Esta es la función createMenus():
void MainWindow::createMenus() { fileMenu = menuBar()->addMenu(tr("&File")); fileMenu->addAction(exitAction); itemMenu = menuBar()->addMenu(tr("&Item")); itemMenu->addAction(deleteAction); itemMenu->addSeparator(); itemMenu->addAction(toFrontAction); itemMenu->addAction(sendBackAction); aboutMenu = menuBar()->addMenu(tr("&Help")); aboutMenu->addAction(aboutAction); }
Creamos los tres menús' del ejemplo.
La función createToolbars() crea las barras de herramientas del ejemplo. Los tres QToolButtons en el colorToolBar, el fontColorToolButton, fillColorToolButton, y lineColorToolButton, son interesantes ya que creamos iconos para ellos dibujando sobre un QPixmap con un QPainter. Mostramos cómo se crea el fillColorToolButton. Este botón permite al usuario seleccionar un color para los elementos del diagrama.
void MainWindow::createToolbars() { ... fillColorToolButton = new QToolButton; fillColorToolButton->setPopupMode(QToolButton::MenuButtonPopup); fillColorToolButton->setMenu(createColorMenu(&MainWindow::itemColorChanged, Qt::white)); fillAction = fillColorToolButton->menu()->defaultAction(); fillColorToolButton->setIcon(createColorToolButtonIcon( ":/images/floodfill.png", Qt::white)); connect(fillColorToolButton, &QAbstractButton::clicked, this, &MainWindow::fillButtonTriggered);
Establecemos el menú del botón de herramientas con setMenu(). Necesitamos que el objeto fillAction QAction apunte siempre a la acción seleccionada del menú. El menú se crea con la función createColorMenu() y, como veremos más adelante, contiene un elemento de menú por cada color que pueden tener los elementos. Cuando el usuario pulsa el botón, que activa la señal clicked(), podemos establecer el color del elemento seleccionado al color de fillAction. Es con createColorToolButtonIcon() que creamos el icono para el botón.
... }
Esta es la función createBackgroundCellWidget():
QWidget *MainWindow::createBackgroundCellWidget(const QString &text, const QString &image) { QToolButton *button = new QToolButton; button->setText(text); button->setIcon(QIcon(image)); button->setIconSize(QSize(50, 50)); button->setCheckable(true); backgroundButtonGroup->addButton(button); QGridLayout *layout = new QGridLayout; layout->addWidget(button, 0, 0, Qt::AlignHCenter); layout->addWidget(new QLabel(text), 1, 0, Qt::AlignCenter); QWidget *widget = new QWidget; widget->setLayout(layout); return widget; }
Esta función crea QWidgets que contienen un botón de herramienta y una etiqueta. Los widgets creados con esta función se utilizan para el elemento widget de fondo de la caja de herramientas.
Aquí está la función createCellWidget():
QWidget *MainWindow::createCellWidget(const QString &text, DiagramItem::DiagramType type) { DiagramItem item(type, itemMenu); QIcon icon(item.image()); QToolButton *button = new QToolButton; button->setIcon(icon); button->setIconSize(QSize(50, 50)); button->setCheckable(true); buttonGroup->addButton(button, int(type)); QGridLayout *layout = new QGridLayout; layout->addWidget(button, 0, 0, Qt::AlignHCenter); layout->addWidget(new QLabel(text), 1, 0, Qt::AlignCenter); QWidget *widget = new QWidget; widget->setLayout(layout); return widget; }
Esta función devuelve un QWidget que contiene un QToolButton con una imagen de uno de los DiagramItems, es decir, formas de diagrama de flujo. La imagen es creada por el DiagramItem a través de la función image(). La clase QButtonGroup nos permite adjuntar un id (int) con cada botón; almacenamos el tipo del diagrama, es decir, el enum DiagramItem::DiagramType. Utilizamos el tipo de diagrama almacenado cuando creamos nuevos elementos de diagrama para la escena. Los widgets creados con esta función se utilizan en la caja de herramientas.
Aquí está la función createColorMenu():
template<typename PointerToMemberFunction> QMenu *MainWindow::createColorMenu(const PointerToMemberFunction &slot, QColor defaultColor) { QList<QColor> colors; colors << Qt::black << Qt::white << Qt::red << Qt::blue << Qt::yellow; QStringList names; names << tr("black") << tr("white") << tr("red") << tr("blue") << tr("yellow"); QMenu *colorMenu = new QMenu(this); for (int i = 0; i < colors.count(); ++i) { QAction *action = new QAction(names.at(i), this); action->setData(colors.at(i)); action->setIcon(createColorIcon(colors.at(i))); connect(action, &QAction::triggered, this, slot); colorMenu->addAction(action); if (colors.at(i) == defaultColor) colorMenu->setDefaultAction(action); } return colorMenu; }
Esta función crea un menú de colores que se utiliza como menú desplegable para los botones de herramientas en colorToolBar. Creamos una acción para cada color que añadimos al menú. Obtenemos los datos de las acciones cuando establecemos el color de los elementos, las líneas y el texto.
Esta es la función createColorToolButtonIcon():
QIcon MainWindow::createColorToolButtonIcon(const QString &imageFile, QColor color) { QPixmap pixmap(50, 80); pixmap.fill(Qt::transparent); QPainter painter(&pixmap); QPixmap image(imageFile); // Draw icon centred horizontally on button. QRect target(4, 0, 42, 43); QRect source(0, 0, 42, 43); painter.fillRect(QRect(0, 60, 50, 80), color); painter.drawPixmap(target, image, source); return QIcon(pixmap); }
Esta función se utiliza para crear el QIcon de la fillColorToolButton, fontColorToolButton, y lineColorToolButton. La cadena imageFile es el texto, el relleno o el símbolo de línea que se utiliza para los botones. Debajo de la imagen dibujamos un rectángulo relleno usando color.
Aquí está la función createColorIcon():
QIcon MainWindow::createColorIcon(QColor color) { QPixmap pixmap(20, 20); QPainter painter(&pixmap); painter.setPen(Qt::NoPen); painter.fillRect(QRect(0, 0, 20, 20), color); return QIcon(pixmap); }
Esta función crea un icono con un rectángulo relleno en el color de color. Se utiliza para crear iconos para los menús de color en el fillColorToolButton, fontColorToolButton, y lineColorToolButton.
Esta es la función backgroundButtonGroupClicked():
void MainWindow::backgroundButtonGroupClicked(QAbstractButton *button) { const QList<QAbstractButton *> buttons = backgroundButtonGroup->buttons(); for (QAbstractButton *myButton : buttons) { if (myButton != button) button->setChecked(false); } QString text = button->text(); if (text == tr("Blue Grid")) scene->setBackgroundBrush(QPixmap(":/images/background1.png")); else if (text == tr("White Grid")) scene->setBackgroundBrush(QPixmap(":/images/background2.png")); else if (text == tr("Gray Grid")) scene->setBackgroundBrush(QPixmap(":/images/background3.png")); else scene->setBackgroundBrush(QPixmap(":/images/background4.png")); scene->update(); view->update(); }
En esta función establecemos el QBrush que se utiliza para dibujar el fondo de la diagramscene. El fondo puede ser una cuadrícula de azulejos azules, grises o blancos, o ninguna cuadrícula. Tenemos QPixmaps de los azulejos de archivos png con los que creamos el pincel.
Cuando se hace clic en uno de los botones del elemento widget con pestañas del fondo, cambiamos el pincel; averiguamos de qué botón se trata comprobando su texto.
Aquí está la implementación de buttonGroupClicked():
void MainWindow::buttonGroupClicked(QAbstractButton *button) { const QList<QAbstractButton *> buttons = buttonGroup->buttons(); for (QAbstractButton *myButton : buttons) { if (myButton != button) button->setChecked(false); } const int id = buttonGroup->id(button); if (id == InsertTextButton) { scene->setMode(DiagramScene::InsertText); } else { scene->setItemType(DiagramItem::DiagramType(id)); scene->setMode(DiagramScene::InsertItem); } }
Esta ranura es llamada cuando un botón en buttonGroup es marcado. Cuando un botón está marcado el usuario puede hacer clic en la vista gráfica y un DiagramItem del tipo seleccionado se insertará en DiagramScene. Debemos hacer un bucle a través de los botones del grupo para desmarcar otros botones ya que sólo se permite marcar un botón a la vez.
QButtonGroup asigna un id a cada botón. Hemos establecido el id de cada botón al tipo de diagrama, tal y como viene dado por DiagramItem::DiagramType que se insertará en la escena cuando se haga clic sobre él. Podemos entonces utilizar el id del botón cuando establecemos el tipo de diagrama con setItemType(). En el caso del texto asignamos un id que tiene un valor que no está en el enum DiagramType.
Aquí está la implementación de deleteItem():
void MainWindow::deleteItem() { QList<QGraphicsItem *> selectedItems = scene->selectedItems(); for (QGraphicsItem *item : std::as_const(selectedItems)) { if (item->type() == Arrow::Type) { scene->removeItem(item); Arrow *arrow = qgraphicsitem_cast<Arrow *>(item); arrow->startItem()->removeArrow(arrow); arrow->endItem()->removeArrow(arrow); delete item; } } selectedItems = scene->selectedItems(); for (QGraphicsItem *item : std::as_const(selectedItems)) { if (item->type() == DiagramItem::Type) qgraphicsitem_cast<DiagramItem *>(item)->removeArrows(); scene->removeItem(item); delete item; } }
Esta ranura borra el elemento seleccionado, si lo hay, de la escena. Borra primero las flechas para evitar borrarlas dos veces. Si el ítem a ser borrado es un DiagramItem, también necesitamos borrar las flechas conectadas a él; no queremos flechas en la escena que no estén conectadas a ítems en ambos extremos.
Esta es la implementación de pointerGroupClicked():
void MainWindow::pointerGroupClicked() { scene->setMode(DiagramScene::Mode(pointerTypeGroup->checkedId())); }
El pointerTypeGroup decide si la escena está en modo ItemMove o InsertLine. Este grupo de botones es exclusivo, es decir, sólo se marca un botón a la vez. Al igual que con el buttonGroup anterior hemos asignado un id a los botones que coincide con los valores del enum DiagramScene::Mode, de modo que podemos utilizar el id para establecer el modo correcto.
Aquí está la ranura bringToFront():
void MainWindow::bringToFront() { if (scene->selectedItems().isEmpty()) return; QGraphicsItem *selectedItem = scene->selectedItems().first(); const QList<QGraphicsItem *> overlapItems = selectedItem->collidingItems(); qreal zValue = 0; for (const QGraphicsItem *item : overlapItems) { if (item->zValue() >= zValue && item->type() == DiagramItem::Type) zValue = item->zValue() + 0.1; } selectedItem->setZValue(zValue); }
Varios elementos pueden colisionar, es decir, superponerse, entre sí en la escena. Se llama a esta ranura cuando el usuario solicita que un elemento se coloque encima de los elementos con los que colisiona. QGrapicsItems tiene un valor z que decide el orden en el que se apilan los elementos en la escena; puedes pensar en él como el eje z en un sistema de coordenadas 3D. Cuando los elementos colisionan, los elementos con valores z más altos se dibujan encima de los elementos con valores más bajos. Cuando traemos un elemento al frente podemos hacer un bucle a través de los elementos con los que colisiona y establecer un valor z que sea más alto que todos ellos.
Aquí está la ranura sendToBack():
void MainWindow::sendToBack() { if (scene->selectedItems().isEmpty()) return; QGraphicsItem *selectedItem = scene->selectedItems().first(); const QList<QGraphicsItem *> overlapItems = selectedItem->collidingItems(); qreal zValue = 0; for (const QGraphicsItem *item : overlapItems) { if (item->zValue() <= zValue && item->type() == DiagramItem::Type) zValue = item->zValue() - 0.1; } selectedItem->setZValue(zValue); }
Esta ranura funciona de la misma manera que bringToFront() descrito anteriormente, pero establece un valor z que es menor que los elementos con los que el elemento que debe ser enviado a la parte posterior colisiona.
Esta es la implementación de itemInserted():
void MainWindow::itemInserted(DiagramItem *item) { pointerTypeGroup->button(int(DiagramScene::MoveItem))->setChecked(true); scene->setMode(DiagramScene::Mode(pointerTypeGroup->checkedId())); buttonGroup->button(int(item->diagramType()))->setChecked(false); }
Este slot es llamado desde DiagramScene cuando un ítem ha sido agregado a la escena. Devolvemos el modo de la escena al modo anterior a la inserción del ítem, que es ItemMove o InsertText dependiendo del botón que esté marcado en el pointerTypeGroup. También debemos desmarcar el botón en el buttonGroup.
Esta es la implementación de textInserted():
void MainWindow::textInserted(QGraphicsTextItem *) { buttonGroup->button(InsertTextButton)->setChecked(false); scene->setMode(DiagramScene::Mode(pointerTypeGroup->checkedId())); }
Simplemente devolvemos el modo de la escena al modo que tenía antes de insertar el texto.
Esta es la implementación de currentFontChanged():
void MainWindow::currentFontChanged(const QFont &) { handleFontChange(); }
Cuando el usuario solicita un cambio de fuente, utilizando uno de los widgets en el fontToolBar, creamos un nuevo objeto QFont y establecemos sus propiedades para que coincidan con el estado de los widgets. Esto se hace en handleFontChange(), así que simplemente llamamos a esa ranura.
Aquí está la ranura fontSizeChanged():
void MainWindow::fontSizeChanged(const QString &) { handleFontChange(); }
Cuando el usuario solicita un cambio de fuente, utilizando uno de los widgets en fontToolBar, creamos un nuevo objeto QFont y establecemos sus propiedades para que coincidan con el estado de los widgets. Esto se hace en handleFontChange(), así que simplemente llamamos a esa ranura.
Esta es la implementación de sceneScaleChanged():
void MainWindow::sceneScaleChanged(const QString &scale) { double newScale = scale.left(scale.indexOf(tr("%"))).toDouble() / 100.0; QTransform oldMatrix = view->transform(); view->resetTransform(); view->translate(oldMatrix.dx(), oldMatrix.dy()); view->scale(newScale, newScale); }
El usuario puede aumentar o disminuir la escala, con el sceneScaleCombo, se dibuja la escena. No es la propia escena la que cambia su escala, sino sólo la vista.
Aquí está la ranura textColorChanged():
void MainWindow::textColorChanged() { textAction = qobject_cast<QAction *>(sender()); fontColorToolButton->setIcon(createColorToolButtonIcon( ":/images/textpointer.png", qvariant_cast<QColor>(textAction->data()))); textButtonTriggered(); }
Esta ranura se llama cuando se pulsa un elemento del menú desplegable del fontColorToolButton. Necesitamos cambiar el icono del botón al color del QAction seleccionado. Mantenemos un puntero a la acción seleccionada en textAction. Es en textButtonTriggered() donde cambiamos el color del texto al color de textAction, por lo que llamamos a esa ranura.
Aquí está la implementación de itemColorChanged():
void MainWindow::itemColorChanged() { fillAction = qobject_cast<QAction *>(sender()); fillColorToolButton->setIcon(createColorToolButtonIcon( ":/images/floodfill.png", qvariant_cast<QColor>(fillAction->data()))); fillButtonTriggered(); }
Esta ranura gestiona las peticiones para cambiar el color de DiagramItems de la misma manera que textColorChanged() lo hace para DiagramTextItems.
Esta es la implementación de lineColorChanged():
void MainWindow::lineColorChanged() { lineAction = qobject_cast<QAction *>(sender()); lineColorToolButton->setIcon(createColorToolButtonIcon( ":/images/linecolor.png", qvariant_cast<QColor>(lineAction->data()))); lineButtonTriggered(); }
Esta ranura gestiona las peticiones para cambiar el color de Arrows de la misma manera que textColorChanged() lo hace para DiagramTextItems.
Esta es la ranura textButtonTriggered():
void MainWindow::textButtonTriggered() { scene->setTextColor(qvariant_cast<QColor>(textAction->data())); }
textAction apunta al QAction del elemento de menú actualmente seleccionado en el menú desplegable de color de fontColorToolButton. Hemos establecido los datos de la acción en el QColor que representa la acción, por lo que podemos simplemente recuperarlo cuando establezcamos el color del texto con setTextColor().
Aquí está la ranura fillButtonTriggered():
void MainWindow::fillButtonTriggered() { scene->setItemColor(qvariant_cast<QColor>(fillAction->data())); }
fillAction apunta al elemento de menú seleccionado en el menú desplegable de fillColorToolButton(). Por lo tanto, podemos utilizar los datos de esta acción cuando establezcamos el color del elemento con setItemColor().
Aquí está la ranura lineButtonTriggered():
void MainWindow::lineButtonTriggered() { scene->setLineColor(qvariant_cast<QColor>(lineAction->data())); }
lineAction apunta al elemento seleccionado en el menú desplegable de lineColorToolButton. Utilizamos sus datos cuando establecemos el color de la flecha con setLineColor().
Aquí está la función handleFontChange():
void MainWindow::handleFontChange() { QFont font = fontCombo->currentFont(); font.setPointSize(fontSizeCombo->currentText().toInt()); font.setWeight(boldAction->isChecked() ? QFont::Bold : QFont::Normal); font.setItalic(italicAction->isChecked()); font.setUnderline(underlineAction->isChecked()); scene->setFont(font); }
handleFontChange() se llama cuando cambia cualquiera de los widgets que muestran las propiedades de la fuente. Creamos un nuevo objeto QFont y establecemos sus propiedades en función de los widgets. Luego llamamos a la función setFont() de DiagramScene; es la escena que establece la fuente del DiagramTextItems que maneja.
Aquí está la ranura itemSelected():
void MainWindow::itemSelected(QGraphicsItem *item) { DiagramTextItem *textItem = qgraphicsitem_cast<DiagramTextItem *>(item); QFont font = textItem->font(); fontCombo->setCurrentFont(font); fontSizeCombo->setEditText(QString().setNum(font.pointSize())); boldAction->setChecked(font.weight() == QFont::Bold); italicAction->setChecked(font.italic()); underlineAction->setChecked(font.underline()); }
Esta ranura se llama cuando se selecciona un elemento en el DiagramScene. En el caso de este ejemplo sólo son elementos de texto los que emiten señales cuando son seleccionados, por lo que no necesitamos comprobar qué tipo de gráfico es item.
Establecemos el estado de los widgets para que coincida con las propiedades de la fuente del elemento de texto seleccionado.
Esta es la ranura about():
void MainWindow::about() { QMessageBox::about(this, tr("About Diagram Scene"), tr("The <b>Diagram Scene</b> example shows " "use of the graphics framework.")); }
Esta ranura muestra una caja acerca del ejemplo cuando el usuario selecciona el ítem de menú acerca del menú de ayuda.
Definición de la Clase DiagramScene
La clase DiagramScene hereda de QGraphicsScene y añade funcionalidad para manejar DiagramItems, Arrows, y DiagramTextItems además de los elementos manejados por su superclase.
class DiagramScene : public QGraphicsScene { Q_OBJECT public: enum Mode { InsertItem, InsertLine, InsertText, MoveItem }; explicit DiagramScene(QMenu *itemMenu, QObject *parent = nullptr); QFont font() const { return myFont; } QColor textColor() const { return myTextColor; } QColor itemColor() const { return myItemColor; } QColor lineColor() const { return myLineColor; } void setLineColor(const QColor &color); void setTextColor(const QColor &color); void setItemColor(const QColor &color); void setFont(const QFont &font); public slots: void setMode(Mode mode); void setItemType(DiagramItem::DiagramType type); void editorLostFocus(DiagramTextItem *item); signals: void itemInserted(DiagramItem *item); void textInserted(QGraphicsTextItem *item); void itemSelected(QGraphicsItem *item); protected: void mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent) override; void mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent *mouseEvent) override; private: bool isItemChange(int type) const; DiagramItem::DiagramType myItemType; QMenu *myItemMenu; Mode myMode; bool leftButtonDown; QPointF startPoint; QGraphicsLineItem *line; QFont myFont; DiagramTextItem *textItem; QColor myTextColor; QColor myItemColor; QColor myLineColor; };
En DiagramScene un clic del ratón puede dar tres acciones diferentes: el elemento bajo el ratón puede ser movido, un elemento puede ser insertado, o una flecha puede ser conectada entre los elementos del diagrama. La acción de un clic del ratón depende del modo, dado por el enum Mode, en el que se encuentre la escena. El modo se establece con la función setMode().
La escena también establece el color de sus elementos y la fuente de sus elementos de texto. Los colores y el tipo de letra utilizados por la escena pueden definirse con las funciones setLineColor(), setTextColor(), setItemColor() y setFont(). El tipo de DiagramItem, dado por la función DiagramItem::DiagramType, que se creará cuando se inserte un elemento se establece con la ranura setItemType().
Las funciones MainWindow y DiagramScene comparten la responsabilidad de la funcionalidad de los ejemplos. MainWindow se encarga de las siguientes tareas: la eliminación de elementos, texto y flechas; mover elementos del diagrama al fondo y al frente; y establecer la escala de la escena.
Implementación de la clase DiagramScene
Comenzamos con el constructor:
DiagramScene::DiagramScene(QMenu *itemMenu, QObject *parent) : QGraphicsScene(parent) { myItemMenu = itemMenu; myMode = MoveItem; myItemType = DiagramItem::Step; line = nullptr; textItem = nullptr; myItemColor = Qt::white; myTextColor = Qt::black; myLineColor = Qt::black; }
La escena utiliza myItemMenu para establecer el menú contextual cuando se crea DiagramItems. Establecemos el modo por defecto a DiagramScene::MoveItem ya que esto da el comportamiento por defecto de QGraphicsScene.
Aquí está la función setLineColor():
void DiagramScene::setLineColor(const QColor &color) { myLineColor = color; if (isItemChange(Arrow::Type)) { Arrow *item = qgraphicsitem_cast<Arrow *>(selectedItems().first()); item->setColor(myLineColor); update(); } }
La función isItemChange devuelve true si un ítem Arrow está seleccionado en la escena en cuyo caso queremos cambiar su color. Cuando el DiagramScene crea y añade nuevas flechas a la escena también utilizará el nuevo color.
Esta es la función setTextColor():
void DiagramScene::setTextColor(const QColor &color) { myTextColor = color; if (isItemChange(DiagramTextItem::Type)) { DiagramTextItem *item = qgraphicsitem_cast<DiagramTextItem *>(selectedItems().first()); item->setDefaultTextColor(myTextColor); } }
Esta función establece el color de DiagramTextItems igual a como setLineColor() establece el color de Arrows.
Esta es la función setItemColor():
void DiagramScene::setItemColor(const QColor &color) { myItemColor = color; if (isItemChange(DiagramItem::Type)) { DiagramItem *item = qgraphicsitem_cast<DiagramItem *>(selectedItems().first()); item->setBrush(myItemColor); } }
Esta función establece el color que usará la escena al crear DiagramItems. También cambia el color de un DiagramItem seleccionado.
Esta es la implementación de setFont():
void DiagramScene::setFont(const QFont &font) { myFont = font; if (isItemChange(DiagramTextItem::Type)) { QGraphicsTextItem *item = qgraphicsitem_cast<DiagramTextItem *>(selectedItems().first()); //At this point the selection can change so the first selected item might not be a DiagramTextItem if (item) item->setFont(myFont); } }
Establece la fuente a utilizar para nuevo y seleccionado, si un elemento de texto está seleccionado, DiagramTextItems.
Esta es la implementación de editorLostFocus() slot:
void DiagramScene::editorLostFocus(DiagramTextItem *item) { QTextCursor cursor = item->textCursor(); cursor.clearSelection(); item->setTextCursor(cursor); if (item->toPlainText().isEmpty()) { removeItem(item); item->deleteLater(); } }
DiagramTextItems Emitir una señal cuando pierden el foco, que está conectado a esta ranura. Eliminamos el elemento si no tiene texto. Si no, perderíamos memoria y confundiríamos al usuario, ya que los ítems se editarán al ser pulsados por el ratón.
La función mousePressEvent() maneja los eventos de pulsación del ratón de forma diferente dependiendo del modo en el que se encuentre DiagramScene. Examinamos su implementación para cada modo:
void DiagramScene::mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent) { if (mouseEvent->button() != Qt::LeftButton) return; DiagramItem *item; switch (myMode) { case InsertItem: item = new DiagramItem(myItemType, myItemMenu); item->setBrush(myItemColor); addItem(item); item->setPos(mouseEvent->scenePos()); emit itemInserted(item); break;
Simplemente creamos un nuevo DiagramItem y lo añadimos a la escena en la posición en la que se pulsó el ratón. Nótese que el origen de su sistema de coordenadas local estará bajo la posición del puntero del ratón.
case InsertLine: line = new QGraphicsLineItem(QLineF(mouseEvent->scenePos(), mouseEvent->scenePos())); line->setPen(QPen(myLineColor, 2)); addItem(line); break;
El usuario añade Arrows a la escena estirando una línea entre los elementos que la flecha debe conectar. El inicio de la línea se fija en el lugar donde el usuario pulsó el ratón y el final sigue al puntero del ratón mientras se mantenga pulsado el botón. Cuando el usuario suelte el botón del ratón se añadirá un Arrow a la escena si hay un DiagramItem bajo el inicio y el final de la línea. Veremos cómo se implementa esto más adelante; aquí simplemente añadimos la línea.
case InsertText: textItem = new DiagramTextItem(); textItem->setFont(myFont); textItem->setTextInteractionFlags(Qt::TextEditorInteraction); textItem->setZValue(1000.0); connect(textItem, &DiagramTextItem::lostFocus, this, &DiagramScene::editorLostFocus); connect(textItem, &DiagramTextItem::selectedChange, this, &DiagramScene::itemSelected); addItem(textItem); textItem->setDefaultTextColor(myTextColor); textItem->setPos(mouseEvent->scenePos()); emit textInserted(textItem);
El DiagramTextItem es editable cuando la bandera Qt::TextEditorInteraction está activada, de lo contrario es movible por el ratón. Siempre queremos que el texto se dibuje encima de los otros elementos de la escena, así que fijamos el valor a un número superior al de los otros elementos de la escena.
default: ; } QGraphicsScene::mousePressEvent(mouseEvent); }
Estamos en modo MoveItem si llegamos al interruptor por defecto; entonces podemos llamar a la implementación QGraphicsScene, que maneja el movimiento de elementos con el ratón. Hacemos esta llamada incluso si estamos en otro modo, lo que hace posible añadir un elemento y luego mantener pulsado el botón del ratón y empezar a mover el elemento. En el caso de los elementos de texto, esto no es posible ya que no propagan eventos de ratón cuando son editables.
Esta es la función mouseMoveEvent():
void DiagramScene::mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent) { if (myMode == InsertLine && line != nullptr) { QLineF newLine(line->line().p1(), mouseEvent->scenePos()); line->setLine(newLine); } else if (myMode == MoveItem) { QGraphicsScene::mouseMoveEvent(mouseEvent); } }
Debemos dibujar la línea si estamos en InsertMode y el botón del ratón está pulsado (la línea no es 0). Como se comentó en mousePressEvent() la línea se dibuja desde la posición en la que se pulsó el ratón hasta la posición actual del ratón.
Si estamos en el modo MoveItem, llamamos a la implementación QGraphicsScene, que maneja el movimiento de elementos.
En la función mouseReleaseEvent() necesitamos comprobar si una flecha debe ser añadida a la escena:
void DiagramScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *mouseEvent) { if (line != nullptr && myMode == InsertLine) { QList<QGraphicsItem *> startItems = items(line->line().p1()); if (startItems.count() && startItems.first() == line) startItems.removeFirst(); QList<QGraphicsItem *> endItems = items(line->line().p2()); if (endItems.count() && endItems.first() == line) endItems.removeFirst(); removeItem(line); delete line;
Primero tenemos que obtener los elementos (si los hay) situados bajo los puntos inicial y final de la línea. La propia línea es el primer elemento en estos puntos, así que la eliminamos de las listas. Como precaución, comprobamos si las listas están vacías, pero esto no debería ocurrir nunca.
if (startItems.count() > 0 && endItems.count() > 0 && startItems.first()->type() == DiagramItem::Type && endItems.first()->type() == DiagramItem::Type && startItems.first() != endItems.first()) { DiagramItem *startItem = qgraphicsitem_cast<DiagramItem *>(startItems.first()); DiagramItem *endItem = qgraphicsitem_cast<DiagramItem *>(endItems.first()); Arrow *arrow = new Arrow(startItem, endItem); arrow->setColor(myLineColor); startItem->addArrow(arrow); endItem->addArrow(arrow); arrow->setZValue(-1000.0); addItem(arrow); arrow->updatePosition(); } }
Ahora comprobamos si hay dos DiagramItems diferentes bajo los puntos inicial y final de las líneas. Si los hay, podemos crear un Arrow con los dos elementos. A continuación se añade la flecha a cada elemento y finalmente a la escena. La flecha debe actualizarse para ajustar sus puntos inicial y final a los elementos. Ajustamos el valor z de la flecha a -1000.0 porque queremos que siempre se dibuje debajo de los ítems.
line = nullptr; QGraphicsScene::mouseReleaseEvent(mouseEvent); }
Aquí está la función isItemChange():
bool DiagramScene::isItemChange(int type) const { const QList<QGraphicsItem *> items = selectedItems(); const auto cb = [type](const QGraphicsItem *item) { return item->type() == type; }; return std::find_if(items.begin(), items.end(), cb) != items.end(); }
La escena tiene selección única, es decir, sólo un elemento puede ser seleccionado en un momento dado. El bucle for hará un bucle una vez con el elemento seleccionado o ninguno si no hay ningún elemento seleccionado. isItemChange() se utiliza para comprobar si existe un elemento seleccionado y además es del diagrama especificado type.
Definición de la Clase DiagramItem
class DiagramItem : public QGraphicsPolygonItem { public: enum { Type = UserType + 15 }; enum DiagramType { Step, Conditional, StartEnd, Io }; DiagramItem(DiagramType diagramType, QMenu *contextMenu, QGraphicsItem *parent = nullptr); void removeArrow(Arrow *arrow); void removeArrows(); DiagramType diagramType() const { return myDiagramType; } QPolygonF polygon() const { return myPolygon; } void addArrow(Arrow *arrow); QPixmap image() const; int type() const override { return Type; } protected: void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override; QVariant itemChange(GraphicsItemChange change, const QVariant &value) override; private: DiagramType myDiagramType; QPolygonF myPolygon; QMenu *myContextMenu; QList<Arrow *> arrows; };
El DiagramItem representa una forma de diagrama de flujo en el DiagramScene. Hereda QGraphicsPolygonItem y tiene un polígono para cada forma. El enum DiagramType tiene un valor para cada una de las formas de diagrama de flujo.
La clase tiene una lista de las flechas que están conectadas a ella. Esto es necesario porque sólo el elemento sabe cuándo se está moviendo (con la función itemChanged() ), momento en el que las flechas deben actualizarse. El elemento también puede dibujarse a sí mismo en un QPixmap con la función image(). Esto se utiliza para los botones de herramientas en MainWindow, ver createColorToolButtonIcon() en MainWindow.
El enum Type es un identificador único de la clase. Se utiliza en qgraphicsitem_cast(), que realiza cambios dinámicos de los elementos gráficos. La constante UserType es el valor mínimo que puede tener un tipo de elemento gráfico personalizado.
Implementación de la clase DiagramItem
Comenzamos con un vistazo al constructor:
DiagramItem::DiagramItem(DiagramType diagramType, QMenu *contextMenu, QGraphicsItem *parent) : QGraphicsPolygonItem(parent), myDiagramType(diagramType) , myContextMenu(contextMenu) { QPainterPath path; switch (myDiagramType) { case StartEnd: path.moveTo(200, 50); path.arcTo(150, 0, 50, 50, 0, 90); path.arcTo(50, 0, 50, 50, 90, 90); path.arcTo(50, 50, 50, 50, 180, 90); path.arcTo(150, 50, 50, 50, 270, 90); path.lineTo(200, 25); myPolygon = path.toFillPolygon(); break; case Conditional: myPolygon << QPointF(-100, 0) << QPointF(0, 100) << QPointF(100, 0) << QPointF(0, -100) << QPointF(-100, 0); break; case Step: myPolygon << QPointF(-100, -100) << QPointF(100, -100) << QPointF(100, 100) << QPointF(-100, 100) << QPointF(-100, -100); break; default: myPolygon << QPointF(-120, -80) << QPointF(-70, 80) << QPointF(120, 80) << QPointF(70, -80) << QPointF(-120, -80); break; } setPolygon(myPolygon); setFlag(QGraphicsItem::ItemIsMovable, true); setFlag(QGraphicsItem::ItemIsSelectable, true); setFlag(QGraphicsItem::ItemSendsGeometryChanges, true); }
En el constructor creamos el polígono del ítem de acuerdo a diagramType. QGraphicsItems no son movibles o seleccionables por defecto, por lo que debemos establecer estas propiedades.
Aquí está la función removeArrow():
void DiagramItem::removeArrow(Arrow *arrow) { arrows.removeAll(arrow); }
removeArrow() se utiliza para eliminar elementos Arrow cuando ellos o DiagramItems a los que están conectados se eliminan de la escena.
Aquí está la función removeArrows():
void DiagramItem::removeArrows() { // need a copy here since removeArrow() will // modify the arrows container const auto arrowsCopy = arrows; for (Arrow *arrow : arrowsCopy) { arrow->startItem()->removeArrow(arrow); arrow->endItem()->removeArrow(arrow); scene()->removeItem(arrow); delete arrow; } }
Esta función es llamada cuando el ítem es removido de la escena y remueve todas las flechas que están conectadas a este ítem. La flecha debe ser eliminada de la lista arrows tanto de su elemento inicial como final. Dado que el ítem inicial o final es el objeto donde esta función es llamada actualmente, tenemos que asegurarnos de trabajar sobre una copia de las flechas ya que removeArrow() está modificando este contenedor.
Aquí está la función addArrow():
void DiagramItem::addArrow(Arrow *arrow) { arrows.append(arrow); }
Esta función simplemente añade el arrow a la lista de elementos arrows.
Aquí está la función image():
QPixmap DiagramItem::image() const { QPixmap pixmap(250, 250); pixmap.fill(Qt::transparent); QPainter painter(&pixmap); painter.setPen(QPen(Qt::black, 8)); painter.translate(125, 125); painter.drawPolyline(myPolygon); return pixmap; }
Esta función dibuja el polígono del ítem en un QPixmap. En este ejemplo usamos esto para crear íconos para los botones de herramientas en la caja de herramientas.
Aquí está la función contextMenuEvent():
void DiagramItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) { scene()->clearSelection(); setSelected(true); myContextMenu->popup(event->screenPos()); }
Mostramos el menú contextual. Como los clics con el botón derecho del ratón, que muestran el menú, no seleccionan elementos por defecto, fijamos el elemento seleccionado con setSelected(). Esto es necesario ya que un elemento debe ser seleccionado para cambiar su elevación con las acciones bringToFront y sendToBack.
Esta es la implementación de itemChange():
QVariant DiagramItem::itemChange(GraphicsItemChange change, const QVariant &value) { if (change == QGraphicsItem::ItemPositionChange) { for (Arrow *arrow : std::as_const(arrows)) arrow->updatePosition(); } return value; }
Si el ítem se ha movido, necesitamos actualizar las posiciones de las flechas conectadas a él. La implementación de QGraphicsItem no hace nada, así que simplemente devolvemos value.
Definición de la Clase DiagramTextItem
La clase TextDiagramItem hereda de QGraphicsTextItem y añade la posibilidad de mover elementos de texto editables. Los QGraphicsTextItems editables están diseñados para estar fijos en su lugar y la edición comienza cuando el usuario hace un solo click sobre el ítem. Con DiagramTextItem la edición comienza con un doble click dejando un solo click disponible para interactuar con él y moverlo.
class DiagramTextItem : public QGraphicsTextItem { Q_OBJECT public: enum { Type = UserType + 3 }; DiagramTextItem(QGraphicsItem *parent = nullptr); int type() const override { return Type; } signals: void lostFocus(DiagramTextItem *item); void selectedChange(QGraphicsItem *item); protected: QVariant itemChange(GraphicsItemChange change, const QVariant &value) override; void focusOutEvent(QFocusEvent *event) override; void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; };
Usamos itemChange() y focusOutEvent() para notificar a DiagramScene cuando el elemento de texto pierde el foco y es seleccionado.
Reimplementamos las funciones que manejan los eventos del ratón para hacer posible alterar el comportamiento del ratón de QGraphicsTextItem.
Implementación de DiagramTextItem
Comenzamos con el constructor:
DiagramTextItem::DiagramTextItem(QGraphicsItem *parent) : QGraphicsTextItem(parent) { setFlag(QGraphicsItem::ItemIsMovable); setFlag(QGraphicsItem::ItemIsSelectable); }
Simplemente establecemos el elemento movible y seleccionable, ya que estas banderas están desactivadas por defecto.
Aquí está la función itemChange():
QVariant DiagramTextItem::itemChange(GraphicsItemChange change, const QVariant &value) { if (change == QGraphicsItem::ItemSelectedHasChanged) emit selectedChange(this); return value; }
Cuando el ítem es seleccionado emitimos la señal selectedChanged. MainWindow utiliza esta señal para actualizar los widgets que muestran propiedades de fuente a la fuente del elemento de texto seleccionado.
Esta es la función focusOutEvent():
void DiagramTextItem::focusOutEvent(QFocusEvent *event) { setTextInteractionFlags(Qt::NoTextInteraction); emit lostFocus(this); QGraphicsTextItem::focusOutEvent(event); }
DiagramScene utiliza la señal emitida cuando el elemento de texto pierde el foco para eliminar el elemento si está vacío, es decir, no contiene texto.
Esta es la implementación de mouseDoubleClickEvent():
void DiagramTextItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) { if (textInteractionFlags() == Qt::NoTextInteraction) setTextInteractionFlags(Qt::TextEditorInteraction); QGraphicsTextItem::mouseDoubleClickEvent(event); }
Cuando recibimos un evento de doble clic, hacemos que el elemento sea editable llamando a QGraphicsTextItem::setTextInteractionFlags(). A continuación, reenviamos el doble clic al propio elemento.
Definición de la clase Arrow
La clase Arrow es un elemento gráfico que conecta dos DiagramItems. Dibuja una punta de flecha a uno de los elementos. Para conseguir esto el elemento necesita pintarse a sí mismo y también reimplementar métodos usados por la escena gráfica para comprobar colisiones y selecciones. La clase hereda del ítem QGraphicsLine, y dibuja la punta de la flecha y se mueve con los ítems que conecta.
class Arrow : public QGraphicsLineItem { public: enum { Type = UserType + 4 }; Arrow(DiagramItem *startItem, DiagramItem *endItem, QGraphicsItem *parent = nullptr); int type() const override { return Type; } QRectF boundingRect() const override; QPainterPath shape() const override; void setColor(const QColor &color) { myColor = color; } DiagramItem *startItem() const { return myStartItem; } DiagramItem *endItem() const { return myEndItem; } void updatePosition(); protected: void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; private: DiagramItem *myStartItem; DiagramItem *myEndItem; QPolygonF arrowHead; QColor myColor = Qt::black; };
El color del elemento puede establecerse con setColor().
boundingRect() y shape() están reimplementados a partir de QGraphicsLineItem y son utilizados por la escena para comprobar colisiones y selecciones.
La llamada a updatePosition() hace que la flecha recalcule su posición y el ángulo de la punta de la flecha. paint() se reimplementa para que podamos pintar una flecha en lugar de sólo una línea entre elementos.
myStartItem y myEndItem son los elementos del diagrama que conecta la flecha. La flecha se dibuja con su cabeza hacia el elemento final. arrowHead es un polígono con tres vértices que usamos para dibujar la cabeza de la flecha.
Implementación de la Clase Flecha
El constructor de la clase Arrow tiene el siguiente aspecto:
Arrow::Arrow(DiagramItem *startItem, DiagramItem *endItem, QGraphicsItem *parent) : QGraphicsLineItem(parent), myStartItem(startItem), myEndItem(endItem) { setFlag(QGraphicsItem::ItemIsSelectable, true); setPen(QPen(myColor, 2, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); }
Establecemos los elementos de diagrama inicial y final de la flecha. La punta de la flecha se dibujará donde la línea intersecte el elemento final.
Aquí está la función boundingRect():
QRectF Arrow::boundingRect() const { qreal extra = (pen().width() + 20) / 2.0; return QRectF(line().p1(), QSizeF(line().p2().x() - line().p1().x(), line().p2().y() - line().p1().y())) .normalized() .adjusted(-extra, -extra, extra, extra); }
Necesitamos reimplementar esta función porque la flecha es más grande que el rectángulo delimitador de QGraphicsLineItem. La escena gráfica utiliza el rectángulo delimitador para saber qué regiones de la escena actualizar.
Esta es la función shape():
QPainterPath Arrow::shape() const { QPainterPath path = QGraphicsLineItem::shape(); path.addPolygon(arrowHead); return path; }
La función shape devuelve un QPainterPath que es la forma exacta del elemento. La función QGraphicsLineItem::shape() devuelve un camino con una línea dibujada con el lápiz actual, por lo que sólo tenemos que añadir la punta de la flecha. Esta función se utiliza para comprobar colisiones y selecciones con el ratón.
Aquí está la ranura updatePosition():
void Arrow::updatePosition() { QLineF line(mapFromItem(myStartItem, 0, 0), mapFromItem(myEndItem, 0, 0)); setLine(line); }
Esta ranura actualiza la flecha estableciendo los puntos inicial y final de su línea en el centro de los elementos que conecta.
Aquí está la función paint():
void Arrow::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { if (myStartItem->collidesWithItem(myEndItem)) return; QPen myPen = pen(); myPen.setColor(myColor); qreal arrowSize = 20; painter->setPen(myPen); painter->setBrush(myColor);
Si los puntos inicial y final colisionan no dibujamos la flecha; el algoritmo que usamos para encontrar el punto en el que la flecha debe dibujarse puede fallar si los puntos colisionan.
Primero establecemos la pluma y el pincel que utilizaremos para dibujar la flecha.
QLineF centerLine(myStartItem->pos(), myEndItem->pos()); QPolygonF endPolygon = myEndItem->polygon(); QPointF p1 = endPolygon.first() + myEndItem->pos(); QPointF intersectPoint; for (int i = 1; i < endPolygon.count(); ++i) { QPointF p2 = endPolygon.at(i) + myEndItem->pos(); QLineF polyLine = QLineF(p1, p2); QLineF::IntersectionType intersectionType = polyLine.intersects(centerLine, &intersectPoint); if (intersectionType == QLineF::BoundedIntersection) break; p1 = p2; } setLine(QLineF(intersectPoint, myStartItem->pos()));
A continuación, tenemos que encontrar la posición en la que dibujar la punta de la flecha. La punta debe dibujarse donde se cruzan la línea y el elemento final. Esto se hace tomando la línea entre cada punto del polígono y comprobando si se cruza con la línea de la flecha. Dado que los puntos inicial y final de la línea están fijados en el centro de los elementos, la línea de la flecha debe intersecar una y sólo una de las líneas del polígono. Tenga en cuenta que los puntos del polígono son relativos al sistema de coordenadas local del elemento. Por lo tanto, debemos añadir la posición del elemento final para que las coordenadas sean relativas a la escena.
double angle = std::atan2(-line().dy(), line().dx()); QPointF arrowP1 = line().p1() + QPointF(sin(angle + M_PI / 3) * arrowSize, cos(angle + M_PI / 3) * arrowSize); QPointF arrowP2 = line().p1() + QPointF(sin(angle + M_PI - M_PI / 3) * arrowSize, cos(angle + M_PI - M_PI / 3) * arrowSize); arrowHead.clear(); arrowHead << line().p1() << arrowP1 << arrowP2;
Calculamos el ángulo entre el eje x y la línea de la flecha. Debemos girar la punta de la flecha a este ángulo para que siga la dirección de la flecha. Si el ángulo es negativo debemos girar la dirección de la flecha.
A continuación podemos calcular los tres puntos del polígono de la punta de flecha. Uno de los puntos es el final de la línea, que ahora es la intersección entre la línea de la flecha y el polígono final. A continuación, borramos el polígono arrowHead de la cabeza de flecha calculada anteriormente y fijamos estos nuevos puntos.
painter->drawLine(line()); painter->drawPolygon(arrowHead); if (isSelected()) { painter->setPen(QPen(myColor, 1, Qt::DashLine)); QLineF myLine = line(); myLine.translate(0, 4.0); painter->drawLine(myLine); myLine.translate(0,-8.0); painter->drawLine(myLine); } }
Si la línea está seleccionada, dibujamos dos líneas de puntos paralelas a la línea de la flecha. No utilizamos la implementación por defecto, que utiliza boundingRect() porque el rectángulo delimitador de QRect es considerablemente mayor que la línea.
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.