Resizing a Qt window when a child element has been removed. - qt

I have a QWidget window, which contains a QSplitter. I have a QStackedLayout (within a QWidget container) on both the left and right of the splitter. The main application resides in the left region, which can trigger QWebViews to appear on the right. When I remove the right QWebView, the Window attempts to resize, but stays at the width of the QWebView. When I try to drag the window, it snaps to position around the left side of the splitter.
I want the window to snap back to only show the left side of the splitter on removing the last widget from the right QStackedWidget. I've tried various incantations, but I don't know Qt very well, and am attempting to learn as I go.
The below code is in Java, using the Jambi wrappers. Concepts should be the same in C++, and I'm able to translate any other language binding to Jambi if necessary.
I attempt to set the window minimum width on add and remove of widgets to the RightStackedLayout, but this breaks the splitter handle in some cases. I consider this a hack workaround. If anyone can point me in the right direction of having changes to the right side of the splitter update the top level window, I'll be very grateful.
// The main Window class
public Window() { // #QWidget
super();
this.setContentsMargins(0, 0, 0, 0);
QHBoxLayout mainLayout = new QHBoxLayout(this);
mainLayout.setContentsMargins(0, 0, 0, 0);
splitter = new QSplitter();
QWidget leftContainer = new QWidget();
QWidget rightContainer = new QWidget();
leftLayout = new QStackedLayout();
rightLayout = new RightStackedLayout();
leftContainer.setLayout(leftLayout);
rightContainer.setSizePolicy(new QSizePolicy(QSizePolicy.Policy.Expanding,QSizePolicy.Policy.Expanding));
rightContainer.setLayout(rightLayout);
splitter.addWidget(leftContainer);
splitter.addWidget(rightContainer);
splitter.setStretchFactor(0, 0); // do not resize left
splitter.setStretchFactor(1, 1); // fully resize right
splitter.setWindowState(WindowState.WindowMaximized);
mainLayout.addWidget(splitter);
}
public void denyWidthChange() { // when right side is closed, prevent user from resizing horizontally
this.setSizePolicy(new QSizePolicy(QSizePolicy.Policy.Fixed,QSizePolicy.Policy.Preferred));
this.splitter.setSizePolicy(new QSizePolicy(QSizePolicy.Policy.Fixed,QSizePolicy.Policy.Preferred));
this.splitter.updateGeometry();
this.updateGeometry();
}
public void allowWidthChange() { // when right side open, allow user to expand horizontally
this.setSizePolicy(new QSizePolicy(QSizePolicy.Policy.Expanding,QSizePolicy.Policy.Expanding));
this.splitter.setSizePolicy(new QSizePolicy(QSizePolicy.Policy.Expanding,QSizePolicy.Policy.Expanding));
this.splitter.updateGeometry();
this.updateGeometry();
}
public void adjustSizes(int w, int h) {
this.resize(w, h);
this.splitter.resize(w, h);
}
// The RightStackedLayout
public void add(QWidget widget) {
Application.window.allowWidthChange();
Application.window.setMinimumWidth(((WebPane)widget).minWidth()+this.rememberWidth); // left side + right side
QWidget current = this.currentWidget();
if (current != null) {
this.rememberWidth = current.size().width(); // remember the user resize preferences
}
int idx = this.indexOf(widget);
if (idx == -1) {
widget.setMinimumWidth(this.rememberWidth);
this.addWidget(widget);
}
this.setCurrentWidget(widget);
}
public void remove(QWidget widget) {
Application.window.allowWidthChange();
this.removeWidget(widget);
this.update();
if (this.count() == 0) {
Log.debug("Last RightWebPane Closing: Shrinking the window");
this.rememberWidth = widget.size().width();
this.activate();
//((QWidget)this.parent()).resize(0, 0);
Application.window.setMinimumWidth(((WebPane)widget).minWidth());
Application.window.adjustSizes(0,Application.window.height());
Application.window.denyWidthChange();
}
}

While I still haven't got a fully functional resizing policy happening, I'm most of the way and figured out the problem I was having had to do with not taking the splitter.handleWidth() into account when attempting to calculate the total width.

Related

How to keep an unmanaged node position fixed when its width/height changes

In JavaFX how do I keep the location (x,y coords) of an unmanaged Node the same when the width/height of the node changes?
The node is unmanaged and laid out using: resizeRelocate. This is working well but sometimes the node changes height or width and I would like to keep the current x,y coords. i.e. the node changes size but doesn't move within the scene.
I've tried listening to the boundsInLocal property and checking for differences in the minY values but the node is still moving and calling resizeRelocate from within the boundsInLocal listener triggers another boundsInLocal update.
What would be the best way to resize and reposition the node so that I keep the x,y coords but can change the width/height?
Here's an example:
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.scene.control.*;
import javafx.stage.*;
import javafx.application.*;
public class SizeTest extends Application
{
public static void main (String[] args)
{
Application.launch (args);
}
#Override
public void start (Stage stage)
{
try
{
VBox b = new VBox ();
VBox other = new VBox ();
other.setManaged (false);
other.setPrefWidth (100);
other.setStyle ("-fx-border-width: 2px; -fx-border-color: red; -fx-padding: 5px;");
b.getChildren ().add (other);
VBox another = new VBox ();
another.setPrefWidth (50);
another.setMinHeight (50);
another.setPrefHeight (50);
another.setStyle ("-fx-background-color: blue;");
Button but = new Button ("Push Me");
but.setOnAction (ev ->
{
Button abut = new Button ("Another one");
other.getChildren ().add (abut);
});
other.getChildren ().addAll (another, but);
other.boundsInLocalProperty ().addListener ((p, oldv, newv) ->
{
double h = other.prefHeight (other.getPrefWidth ());
other.resizeRelocate (other.getBoundsInParent ().getMinX (),
other.getBoundsInParent ().getMinY (),
other.getBoundsInParent ().getWidth (),
h);
});
Scene sc = new Scene (b);
stage.setScene (sc);
stage.sizeToScene ();
stage.show ();
Platform.runLater (() ->
{
double h = other.prefHeight (other.getPrefWidth ());
other.resizeRelocate (100,
100,
other.getPrefWidth (),
h);
});
} catch (Exception e) {
e.printStackTrace ();
}
}
}
When you press the "Push Me" button another button is added to the container, it then overflows. I'd like the "other" container to instead resize itself to its preferred size but keep its x,y coords.
The listener for the boundsInLocal property sort of does this but you'll notice the jump in position and incorrect sizing. Removing the listener sizes and positions the node correctly but adding the new button overflows the container.
At this point I'm thinking of creating my own layout manager for this behavior.
My final solution to this was to use a javafx.scene.layout.Pane. My use case here was to have a number of popups appear above the content. I used a StackPane with the "popup pane" being the top level with a transparent background. The "popups" are added to the Pane and positioned with Node.relocate(x,y). The pane handles sizing and will keep the popup at the specified x,y coords. The pane keeps the popup at its preferred size so manual sizes changes should be made through the setPref* methods.
Addendum: while the above method works, because the popup pane would be a sibling of other children of the StackPane then events aren't delivered to the "lower levels", i.e. whatever is below the popup pane.
To get around this I added the content pane (the lower level) to the Pane and then tied the content pane's pref width/height to the size of the Pane.
i.e.
this.popupPane = new Pane ();
this.contentPane = new SplitPane ();
this.contentPane.prefWidthProperty ().bind (this.popupPane.widthProperty ());
this.contentPane.prefHeightProperty ().bind (this.popupPane.heightProperty ());
this.popupPane.getChildren ().add (this.contentPane);

How to show a QDialog in center of a QGraphicsView?

I have a QGraphicsView subclass where, on context menu on an item, I want to show a Properties dialog.
I would like the dialog to be centered in the view...
As I have it now, with no parent, it is shown in the center of the screen.
MyView::MyView(QWidget *parent) : QGraphicsView(parent) {}
void MyView::showProperties()
{
TabDialog *tabDialog = new TabDialog(); // shows in center of screen
// TabDialog *tabDialog = new TabDialog(this); // doesn't show at all
// TabDialog *tabDialog = new TabDialog((QWidget*)this->parent()); // doesn't show at all
tabDialog->setWindowFlags(Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint);
tabDialog->exec();
delete tabDialog;
}
The view is placed in a groupbox... so the parent is the groupbox...
How can I call the dialog using a parent ?
You have to set the dialog to be a top-level window, not simply a child widget that would be embedded in your view. Your setWindowsFlags call resets the relevant flags from the dialog. You need to manually preserve them.
You also should never use exec() to reenter the event loop since this requires that a lot of your other code needs to be reentrant as well.
A minimal fix would look like:
void MyView::showProperties()
{
auto dialog = new TabDialog(this);
dialog->setWindowFlags(Qt::Dialog |
Qt::WindowCloseButtonHint |
Qt::WindowSystemMenuHint);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->show();
}
Perhaps you'd want to cache the dialog:
class MyView : public ... {
QPointer<TabDialog> m_tabDialog; // nulls itself when the dialog perishes
QTimer m_tabDialogTimer;
...
};
MyView::MyView(...) {
m_tabDialogTimer->setSingleShot(true);
...
}
void MyView::showProperties() {
if (! m_tabDialog) {
m_tabDialog = new TabDialog(this);
m_tabDialog->setWindowFlags(Qt::Dialog |
Qt::WindowCloseButtonHint |
Qt::WindowSystemMenuHint);
QObject::connect(&m_tabDialogTimer, &QTimer::timeout,
m_tabDialog, &QObject::deleteLater);
QObject::connect(m_tabDialog, &QDialog::finished, [&this](int){
// the dialog gets deleted 120 seconds after last use
m_tabDialogTimer.start(120);
});
}
m_tabDialogTimer.stop(); // reset pending timeout, if any
m_tabDialog->show();
}

How to ensure, the entire area in QScrollArea was displayed?

I'm displaying some information to the user in QScrollArea.
The user should have seen all contents, before she can proceed (at least the content should have been scrolled to the end)
How could I detect this in an easily?
Is the reimplementing of virtual void scrollContentsBy (int dx,int dy) the only way?
EDIT
I was able to solve it, but had to use some workarounds:
Scroll-action value sent by the signal actionTriggered(int) had never the value QAbstractSlider::SliderToMaximum (Qt4.8, Windows 7). So I've checked manually, if the slider value is close to maximum.
Even if scroll-bar widget was dragged by mouse till the bottom, the value of the scroll-bar is never the maximum. Only if the scroll-bar widget is moved by any other event such as arrow-down or mouse wheel, the value may become maximum. I've work-arounded it with recheckPosition()
I hope, there are better solutions.
void NegativeConfirmation::recheckPosition()
{
processScrollAction(1);
}
void NegativeConfirmation::processScrollAction( int evt)
{
if ( evt == QAbstractSlider::SliderToMaximum) // Have not managed to receive this action
{
ui->bConfirm->setEnabled(true);
}
//Another approach
QWidget * sw = ui->scrollArea->widget();
if ( sw ) //any content at all ?
{
QScrollBar * sb = ui->scrollArea->verticalScrollBar();
if ( sb )
{
int sbv = sb->value();
int sbm = sb->maximum()-10;
if ( sbm>0 && sbv >= sbm )
{
ui->bConfirm->setEnabled(true);
}
else
{
QTimer::singleShot(1000, this, SLOT(recheckPosition()));
}
}
}
}
QScrollArea inherits QAbstractSlider which provides this signal: -
void QAbstractSlider::actionTriggered(int action)
Where action can be QAbstractSlider::SliderToMaximum.
I expect you can connect to the this signal and test when the action is QAbstractSlider::SliderToMaximum, representing that the user has scrolled to the bottom.

Accepting drops on a QGraphicsScene

I'm trying to implement drag'n'drop for a QGraphicsScene. Here are the events I've overloaded:
void TargetScene::dragEnterEvent(QGraphicsSceneDragDropEvent *event) {
bool acceptDrag = false;
const QMimeData* mime = event->mimeData();
// Is an image present?
if (mime->hasImage()) {
QImage img = qvariant_cast<QImage>(mime->imageData());
dragPix = QPixmap::fromImage(img);
acceptDrag = !dragPix.isNull();
}
event->setAccepted(acceptDrag);
}
void TargetScene::dropEvent(QGraphicsSceneDragDropEvent *event) {
// Add dragged pixmap to scene
QGraphicsPixmapItem* newPix = this->addPixmap(dragPix);
newPix->setPos(event->pos().x(), event->pos().y());
}
The scene still won't accept drops. I'm guessing that's because I can't do setAcceptDrops(true) on my QGraphicsScene.
How do I accept drops on a graphics scene?
The trick here is to ALSO accept the event in the QGraphicsScene::dragMoveEvent()!
The reason is the DEFAULT implementation which ignores drag and drop events if there is no item under the mouse!
Also refer to: http://www.qtcentre.org/threads/8022-QGraphicsScene-doesn-t-accept-Drops
Cheers

Qt painted content goes lost

I am writing an info-screen program. I created a full-screen widget and draw contents onto it.
In order to extend the life cycle of the TFT-display device, I want to implement a pixel-shifting feature. With other words, in every X minutes, I shift the screen to left/right/top/down for Y pixels.
My approach is as follows:
I use two layers (two QWidget).
I paint contents on the top layer.
When a pixel-shifting is performed, I just move the top layer for specified offset.
And then fill a background color to the bottom layer.
However, I found a problem:
If I move up the top layer for 10 pixels, the 10-pixel-content goes out of the screen. But when I move this layer down for 10 pixels. The 10-pixel-content will not be updated, it is gone.
How can I keep these 10-pixel-content? Is there any magic widget flag to solve this problem?
UPDATE 1:
The code is written in language D, but it is easy to understand:
class Canvas: QWidget
{
private QPixmap content;
this(QWidget parent)
{
super(parent);
setAttribute(Qt.WA_OpaquePaintEvent, true);
}
public void requestForPaint(QPixmap content, QRegion region)
{
this.content = content;
update(region);
}
protected override void paintEvent(QPaintEvent event)
{
if (this.content !is null)
{
QPainter painter = new QPainter(this);
painter.setClipping(event.region);
painter.fillRect(event.region.boundingRect, new QColor(0, 0, 0));
painter.drawPixmap(event.region.rect, this.content);
this.content = null;
painter.setClipping(false);
}
}
}
class Screen: QWidget
{
private Canvas canvas;
this()
{
super(); // Top-Level widget
setAutoFillBackground(True);
this.canvas = new Canvas(this);
showFullScreen();
}
public void requestForPaint(QPixmap content, QRegion region)
{
this.canvas.requestForPaint(content, region);
}
private updateBackgroundColor(QColor backgroundColor)
{
QPalette newPalette = palette();
newPalette.setColor(backgroundRole(), backgroundColor);
setPalette(newPalette);
}
public shiftPixels(int dx, int dy)
{
this.canvas.move(dx, dy);
updateBackgroundColor(new QColor(0, 0, 0)); // Just a demo background color
}
}
Screen screen = new Screen;
screen.requestForPaint(some_content, some_region);
screen.shiftPixels(0, -10);
screen.shiftPixels(0, 10);
Looking at the code, my first guess is that your region might be wrong. Try repainting the whole widget each time, and see if that solves the missing 10 pixel problem. If it does, then try working out why your region isn't covering the newly exposed portion.
One possibility along those lines: I notice in your Screen::requestForPaint method that you directly call the Canvas::requestForPaint without doing anything with the region. In Qt, the coordinates for anything like that are often assumed to be local, so if you don't account for the current position of the canvas widget, you might get an incorrect region.
Why not setting the position of the widget directly...? Another options might be using QPainter::translate(-1,-1) or something similar.

Resources