Scenario
In a screen I have 2 managers: 1) menu manager at the top and 2) body manager that has info/button elements. The menu manager does custom drawing so that its menu elements (LabelFields) are properly spaced.
Core Issue - Manager and subfield drawing order
The screen draws fine except when the user preforms an action (clicks a button) that results in an element withing the body manager being added/removed. Once field elements are added/removed from the body, the order in which the menu is drawn gets mixed up.
When the body manager adds or removes a field, instead of the menu manager drawing itself and then its sub elements (label fields), the menu manager begins to draw its sub elements and then itself; thus painting on top of the label fields and making them look like they've disappeared.
Comments
Already tried invalidate and other options -- I've tried to call invalidate, invalidateall, updateDisplay... after adding/removing field elements from body. All without success.
Removing custom sublayout works -- The only way that I can resolve this issue is to remove the menu managers custom sublayout logic. Unfortunately the menu system then draws in a traditional manner and does not provide enough spacing.
Below is the sublayout code for the menu manager, am I missing something here?
public void sublayout(int iWidth, int iHeight)
{
final int iNumFields = getFieldCount();
int maxHeight = 0;
final int segmentWidth = iWidth / iNumFields;
final int segmentWidthHalf = segmentWidth / 2;
for (int i = 0; i < iNumFields; i++)
{
final Item currentField = (Item)this.getField(i);
// 1. Use index to compute bounds of the field
final int xSegmentTrueCenter = segmentWidth * i + segmentWidthHalf;
// 2. center field inbetween bounds using field width (find fill width of text)
final int xFieldStart = xSegmentTrueCenter - currentField.getFont().getAdvance(currentField.getText())/2;
// set up position
setPositionChild(currentField, xFieldStart, getContentTop() + MenuAbstract.PADDING_VERTICAL);
// allow child to draw itself
layoutChild(currentField, iWidth, currentField.getHeight());
// compute max height of the field
//int fieldheight = currentField.getHeight();
maxHeight = currentField.getHeight() > maxHeight
? currentField.getHeight() + 2 * MenuAbstract.PADDING_VERTICAL
: maxHeight;
}
this.setExtent(iWidth, maxHeight);
}
Final Questions
Ultimately I want to keep the custom layout of the menu manager while being allowed to redraw field elements. Here are my final questions:
Have you experienced this before?
Why would the menu manager begin drawing in the wrong order when a field element is added/remove to the screen?
Does the native Manager.sublayout() do something that I'm not to maintain drawing order?
I haven't seen the behavior you describe, but the following line is a little troubling:
// allow child to draw itself
layoutChild(currentField, iWidth, currentField.getHeight());
getHeight() shouldn't return a sensible value until the field has had setExtent called through the layoutChild method. Though I'd expect that it would cause problems in all cases - not sure why this would work the first time around. In your logic I think you can safely just use iHeight instead of currentField.getHeight() in that line. The field will only make itself as big as it needs to be - it won't use all of iHeight unless it's something like a VerticalFieldManager
Related
In a C# Windows Forms app, I have a Form with a Panel with a PictureBox on it. The PictureBox is twice as wide as the Panel and has a graphics drawing in it. On scaling etc. I set AutoScrollPosition to keep the section of interest in the middle of the Panel: no problem. My problem is: when the app starts I want the Panel to show a section in the middle of the drawing, rather than the left hand side.
In the Form constructor I have:
panel1.AutoScroll = true;
panel1.AutoScrollPosition = new Point(100, 0);
textBox1.Text = panel1.AutoScrollPosition.ToString();
But on starting the app, the TextBox shows (0, 0) and the initial scroll position is at the left.
So, for test, I added a button which when pressed also executes:
panel1.AutoScrollPosition = new Point(100, 0);
textBox1.Text = panel1.AutoScrollPosition.ToString();
The TextBox then shows (100, 0) and the panel is scrolled as expected.
I makes no difference whether or not the AutoScrollPosition line is included in the constructor.
What must I do to initialise the scroll position without user interaction?
Finally solved it: you override OnLoad and set AutoScrollPosition in that, e.g.:
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
int yScroll = yToCentre - panel1.Height / 2;
panel1.AutoScrollPosition = new Point(0, yScroll);
}
as the Toolbar or Titlearea on scroll animation feature is referenced in the last section of the Toolbar API, and also in this great video tutorial (starting at about min 45), the animation works well under given circumstances.
I was not able to find any documentation about what these have to be, however I found one circumstance, in which it does not work. Here is a working example to demonstrate the problem:
Form hi = new Form("Title", new BoxLayout(BoxLayout.Y_AXIS));
EncodedImage placeholder = EncodedImage
.createFromImage(Image.createImage(hi.getWidth(), hi.getWidth() / 5, 0xffff0000), true);
URLImage background = URLImage.createToStorage(placeholder, "400px-AGameOfThrones.jpg",
"http://awoiaf.westeros.org/images/thumb/9/93/AGameOfThrones.jpg/400px-AGameOfThrones.jpg");
background.fetch();
Style stitle = hi.getToolbar().getTitleComponent().getUnselectedStyle();
stitle.setBgImage(background);
stitle.setBackgroundType(Style.BACKGROUND_IMAGE_SCALED_FILL);
stitle.setPaddingUnit(Style.UNIT_TYPE_DIPS, Style.UNIT_TYPE_DIPS, Style.UNIT_TYPE_DIPS, Style.UNIT_TYPE_DIPS);
stitle.setPaddingTop(15);
// hi.setLayout(new BorderLayout()); // uncomment this for the animation to break
Container contentContainer = new Container(BoxLayout.y());
contentContainer.setScrollableY(true);
// add some elements so we have something to scroll
for (int i = 0; i < 50; i++)
contentContainer.add(new Label("Entry " + i));
hi.add(contentContainer);
// hi.add(BorderLayout.CENTER, contentContainer); // use this line instead of the above for the animation to break
ComponentAnimation anim = hi.getToolbar().getTitleComponent().createStyleAnimation("Title", 200);
hi.getAnimationManager().onTitleScrollAnimation(anim);
hi.show();
With my current app and the codesample from the Toolbar API (which is roughly adapted here), I found out that the onScrollAnimation event is not being called, when a scroll occurs inside a BorderLayout. Even when I have a separate container, which is not the contentpane itself, and I set setScrollableY(true); to true, the animation works properly. The animation stops working, when this very container is put into Center of the Form, via Borderlayout. in the example above, the layout is exactly the same, as there are no other components in other areas of course, but it breaks the animation.
How to solve this? In my app, I have the need for a BorderLayout but still want to use this cool feature. Also, this is a very un-intuitive feature, if it works for some, but not all layouts. It should be completely layout-agnostic and work in every case.
Thank you.
The adapter is bound to the forms content pane scrolling so it won't work if you have a border layout in here. In that case scrolling isn't detected because the code just isn't aware of the scrolling. It would need to track the scrolling of any component in the UI to detect that scrolling.
as hinted by Shai, the solution is the following:
hi.setLayout(new BorderLayout());
Container contentContainer = new Container(BoxLayout.y());
contentContainer.setScrollableY(true);
// add some elements so we have something to scroll
for (int i = 0; i < 50; i++)
contentContainer.add(new Label("Entry " + i));
hi.add(BorderLayout.CENTER, contentContainer); // use this line instead of the above for the animation to break
ComponentAnimation anim = hi.getToolbar().getTitleComponent().createStyleAnimation("Title", 200);
hi.getAnimationManager().onTitleScrollAnimation(contentContainer, anim);
instead of using the onTitleScollAnimation to just add the animation, provide your own scrollable "body" or content container as the first argument, appended by the animation(s).
I have 6 textboxes at the top of the screen that update an entire column(one textbox per column) based on any changes. I was selecting the columns based on their class (.l#). Here is the code (issues to follow):
function UpdateField() {
var ctrl = this;
var id = parseInt(ctrl.id.replace("item", ""), 10) - 1;
var bound = [".l1", ".l7", ".l8", ".l9"];
var fields = $(bound[id]);
for (var i = 0; i < fields.length; i++)
{
fields[i].innerHTML = $(ctrl).val();
}
};
which is bound to the keyup event for the text areas. Issues are:
1) initially fields.length was -1 as I didn't want to put data in the "add new
row" section at the bottom. However, when running it, I noticed the
final "real" record wasn't being populated. Also, when stepping through, I
noticed that the "new row" field was before the "last row" field.
2) when doing it this way, it is purely superficial: if I double click the field,
the real data hasn't been changed.
so in the grand scheme of things, I know that I was doing it wrong. I'm assuming it involves updating the data and then forcing a render, but I'm not certain.
Figured out how to do it. Modified the original code this way:
function UpdateField() {
var ctrl = this;
var id = parseInt(ctrl.id.replace("item", ""), 10) - 1;
var bound = ['title1', 'title2', 'title3', 'title4'];
var field = bound[id];
for (var i = 0; i < dataView.getLength(); i++)
{
var item = dataView.getItem(i);
item[field] = $(ctrl).val();
dataView.updateItem(i, item);
}
grid.invalidate();
};
I have 6 textboxes (item1-item6) that "bind" to fields in the sense that if I change data in a textbox, it updates all of the rows and any new rows added also have this data.
Parts where the two issues can be explained this way:
1) to work around that, though still it would be a presentational fix and not a real updating of the underlying data, one could force it to ignore if it had the active class attached. Extra work, and not in the "real" direction one is going for (masking the field).
2) It was pretty obvious with the original implementation (though it was all I could figure out via Chrome Dev Tools that I could modify at the time) that it was merely updating a div's content and not actually interacting with the data underneath. Would look nice, and perhaps one could just pull data from the item1-item6 boxes in place of the column if it is submitted, but if someone attempts to modify the cell, they'll be looking at the real data again.
I had created a HorizontalFieldManager & added BitmapFields in that.
In Blackberry Storm, Display.getWidth() is 480. In that I want to use first 450 to add some BitmapFields at LHS of screen which I m creating at runtime & add 2 BitmapFields at start at RHS of Screen.
2 BimapFields which I want to show at start r added in Constructor & other BitmapFields which I m creating at run time r added afterwords like..
class MyCanvas extends MainScreen
{
MyCanvas()
{
hfm_BitmapField = new HorizontalFieldManager(){
protected void sublayout(int maxWidth, int maxHeight) {
super.sublayout(maxWidth, maxHeight);
setExtent(Display.getWidth()-30, 60);
}
};
startBitmap = Bitmap.getBitmapResource("start.png");
startBitmapField = new BitmapField(startBitmap, BitmapField.ACTION_INVOKE | BitmapField.FIELD_HCENTER | BitmapField.FIELD_RIGHT);
hfm_BitmapField.add(startBitmapField);
endBitmap = Bitmap.getBitmapResource("end.png");
endBitmapField = new BitmapField(endBitmap, BitmapField.ACTION_INVOKE | BitmapField.FIELD_HCENTER | BitmapField.FIELD_RIGHT);
hfm_BitmapField.add(endBitmapField);
drawBitmap();
}
public void drawBitmap()
{
bitmap[i] = new Bitmap(50, 50);
Graphics g = new Graphics(bitmap[i]);
g.drawLine(5,5,25,25);
bitmapField[i] = new BitmapField(bitmap[i]);
synchronized(UiApplication.getEventLock()) { hfm.add(bitmapField[i]); }
}
I want startBitmapField & endBitmapField at RHS & bitmapField[i] which I m creating at runtime at LHS of HorizontalFieldManagers.
I m thinking to add 2 HorizontalFieldManagers. 1 for bitmapField[i] & 1 for startBitmapField & endBitmapField. But how to add 2 HorizontalFieldManagers or any other FieldManagers in a row?
Any solution? How to do it?
You can put the 2 horizontal field managers inside another HorizontalFieldManager.
Rather than use the alignment flags try adding to your sublayout method.
For each child of your Manager (hfm) you need to call setPositionChild. So if you want it right aligned and vertically centred you would do something like:
setPositionChild(deleteButton, hfm.getPreferredWidth() - deleteButton.getPreferredWidth(), (hfm.getPreferredHeight() / 2) - (deleteButton.getPreferredHeight() / 2));
This would set the top left hand corner of the delete button to be at the correct position such that it is right aligned and vertically centred within the hfm.
There is going to be a problem if you see the application in touch. You must have each of your customized field in a separate field manager to avoid using touch event. If you use layoutChild instead of super.sublayout(width,height), this will disable the navigation in the screen, so avoid using it. Use navigationMovement to customize your navigation of your fields. More: If you do not use super.sublayout function, it might not layout some of your fields, hence it is recommended that you use it.
More: use Storm emulator for all your touch based application, and 4.5 Pearl emulator JDEs for all the other releases, for the compatibility issues.
How is it possible to maintain widgets aspect ratio in Qt and what about centering the widget?
You don't have to implement your own layout manager. You can do with inheriting QWidget and reimplementing
int QWidget::heightForWidth( int w ) { return w; }
to stay square. However, heightForWidth() doesn't work on toplevel windows on X11, since apparently the X11 protocol doesn't support that. As for centering, you can pass Qt::AlignCenter as the third parameter of QBoxLayout::addWidget() or the fifth parameter of QGridLayout::addWidget().
Note: In newer versions of Qt at least, QWidget does not have the heightForWidth or widthForHeight anymore (so they cannot be overriden), and therefore setWidthForHeight(true) or setHeightForWidth(true) only have an effect for descendants of QGraphicsLayout.
The right answer is to create your custom layout manager. That is possible by subclassing QLayout.
Methods to implement when subclassing QLayout
void addItem(QLayoutItem* item);
Adds item to layout.
int count() const;
Returns the item count.
QLayoutItem* itemAt(int index) const;
Returns item reference at index or 0 if there's none.
QLayoutItem* takeAt(int index);
Takes and returns item from the layout from index or returns 0 if there is none.
Qt::Orientations expandingDirections() const;
Returns the layouts expanding directions.
bool hasHeightForWidth() const;
Tells if the layout handles height for width calculation.
QSize minimumSize() const;
Returns the layouts minimum size.
void setGeometry(const QRect& rect);
Sets the geometry of the layout and the items inside it. Here you have to maintain the aspect ratio and do the centering.
QSize sizeHint() const;
Returns the preferred size for the layout.
Further reading
Maintaining square form for a widget in Qt # Forum Nokia
Implementing a layout manager in Qt # Forum Nokia
Writing custom layout managers # Qt documentation
Calling resize() from within resizeEvent() has never worked well for me -- at best it will cause flickering as the window is resized twice (as you have), at worst an infinite loop.
I think the "correct" way to maintain a fixed aspect ratio is to create a custom layout. You'll have to override just two methods, QLayoutItem::hasHeightForWidth() and QLayoutItem::heightForWidth().
I too was trying to achieve the requested effect: a widget that keeps a fixed aspect ratio while staying centred in its allocated space. At first I tried other answers from this question:
implementing heightForWidth and hasHeightForWidth as suggested by marc-mutz-mmutz simply didn't work for me.
I briefly looked at implementing a custom layout manager, but all Bleadof's links were dead, and when I found the documentation and read through it, it looked way too complicated for what I was trying to achieve.
I ended up creating a custom widget that responds to resizeEvent and uses setContentsMargin to set margins such that the remaining content area keeps the desired ratio.
I found I also had to set the widget's size policy to QSizePolicy::Ignored in both directions to avoid odd resizing issues resulting from the size requests of child widgets—the end result is that my widget accepts whatever size its parent allocates to it (and then sets its margins as described above to keep the desired aspect ratio in its content area).
My code looks like this:
from PySide2.QtWidgets import QWidget, QSizePolicy
class AspectWidget(QWidget):
'''
A widget that maintains its aspect ratio.
'''
def __init__(self, *args, ratio=4/3, **kwargs):
super().__init__(*args, **kwargs)
self.ratio = ratio
self.adjusted_to_size = (-1, -1)
self.setSizePolicy(QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored))
def resizeEvent(self, event):
size = event.size()
if size == self.adjusted_to_size:
# Avoid infinite recursion. I suspect Qt does this for you,
# but it's best to be safe.
return
self.adjusted_to_size = size
full_width = size.width()
full_height = size.height()
width = min(full_width, full_height * self.ratio)
height = min(full_height, full_width / self.ratio)
h_margin = round((full_width - width) / 2)
v_margin = round((full_height - height) / 2)
self.setContentsMargins(h_margin, v_margin, h_margin, v_margin)
(Obviously, this code is in Python, but it should be straightforward to express in C++ or your language of choice.)
In my case overriding heightForWidth() doesn't work. And, for someone, it could be helpful to get working example of using resize event.
At first subclass qObject to create filter. More about event filters.
class FilterObject:public QObject{
public:
QWidget *target = nullptr;//it holds a pointer to target object
int goalHeight=0;
FilterObject(QObject *parent=nullptr):QObject(parent){}//uses QObject constructor
bool eventFilter(QObject *watched, QEvent *event) override;//and overrides eventFilter function
};
Then eventFilter function. It's code should be defined outside of FilterObject definition to prevent warning. Thanks to this answer.
bool FilterObject::eventFilter(QObject *watched, QEvent *event) {
if(watched!=target){//checks for correct target object.
return false;
}
if(event->type()!=QEvent::Resize){//and correct event
return false;
}
QResizeEvent *resEvent = static_cast<QResizeEvent*>(event);//then sets correct event type
goalHeight = 7*resEvent->size().width()/16;//calculates height, 7/16 of width in my case
if(target->height()!=goalHeight){
target->setFixedHeight(goalHeight);
}
return true;
};
And then in main code create FilterObject and set it as EventFilter listener to target object. Thanks to this answer.
FilterObject *filter = new FilterObject();
QWidget *targetWidget = new QWidget();//let it be target object
filter->target=targetWidget;
targetWidget->installEventFilter(filter);
Now filter will receive all targetWidget's events and set correct height at resize event.