The basic idea is that you need two different scrolling containers, one with the fixed column(s), one with the scrolling columns. Each of these need to be in a distinct viewport, so the standard Grid/GridView doesn't work well with this - they make assumptions about how scrolling should behave, so simply subclassing one or both is likely to be fairly involved.
Instead, you can build two grids, one for the locked columns, one for the scrolling ones. Each can deal with their own ColumnConfig
classes, to draw headers and rows, and will be linked to the same ListStore
to ensure their data is in sync - changes in the store will be passed along to both listening grids.
To get the full effect, some additional wiring will be needed:
- Linking scrolling. Listen to the
BodyScrollEvent
from each Grid, and scroll the other to the same place (changing only top
, not left
, since you don't want one to control the other).
- Sizing is the second big piece - both grids need their scrollable height to be the same, but the horizontal scrolling one needs a buffer on the bottom when it is actually showing that scrollbar. Usually the Grid is told to size based on its parent's instructions, though sometimes you directly size the Grid - in this case, this step is not needed, just size the two grids slightly differently. Otherwise, you'll need to structure the layout to properly configure this.
- Finally, the locked column needs its vertical scrollbar to be hidden away - the user has no need to see two vertical scrollbars.
This covers the basic use case, but doesn't deal with things like alternate GridView
implementations - GroupingView
and subclasses will need to link expanding (and to hide the group headings so they don't appear twice, plus deal with the fact that the group row shouldn't get split when the second half scrolls sideways), TreeGridView
and TreeGrid
will need to link expanding nodes and hide the tree +/- icons from the second grid.
Here's this basic set of modifications applied to the basic grid example at http://www.sencha.com/examples/#ExamplePlace:basicgrid. To help avoid confusing the issue, I've deleted a number of other features in that grid such as tooltips and changing the selection model:
public class GridExample implements IsWidget, EntryPoint {
private static final StockProperties props = GWT.create(StockProperties.class);
private ContentPanel root;
@Override
public Widget asWidget() {
if (root == null) {
final NumberFormat number = NumberFormat.getFormat("0.00");
ColumnConfig<Stock, String> nameCol = new ColumnConfig<Stock, String>(props.name(), 50, SafeHtmlUtils.fromTrustedString("<b>Company</b>"));
ColumnConfig<Stock, String> symbolCol = new ColumnConfig<Stock, String>(props.symbol(), 100, "Symbol");
ColumnConfig<Stock, Double> lastCol = new ColumnConfig<Stock, Double>(props.last(), 75, "Last");
ColumnConfig<Stock, Double> changeCol = new ColumnConfig<Stock, Double>(props.change(), 100, "Change");
changeCol.setCell(new AbstractCell<Double>() {
@Override
public void render(Context context, Double value, SafeHtmlBuilder sb) {
String style = "style='color: " + (value < 0 ? "red" : "green") + "'";
String v = number.format(value);
sb.appendHtmlConstant("<span " + style + " qtitle='Change' qtip='" + v + "'>" + v + "</span>");
}
});
ColumnConfig<Stock, Date> lastTransCol = new ColumnConfig<Stock, Date>(props.lastTrans(), 100, "Last Updated");
lastTransCol.setCell(new DateCell(DateTimeFormat.getFormat("MM/dd/yyyy")));
List<ColumnConfig<Stock, ?>> l = new ArrayList<ColumnConfig<Stock, ?>>();
//Remove name from main set of columns
// l.add(nameCol);
l.add(symbolCol);
l.add(lastCol);
l.add(changeCol);
l.add(lastTransCol);
//create two column models, one for the locked section
ColumnModel<Stock> lockedCm = new ColumnModel<Stock>(Collections.<ColumnConfig<Stock, ?>>singletonList(nameCol));
ColumnModel<Stock> cm = new ColumnModel<Stock>(l);
ListStore<Stock> store = new ListStore<Stock>(props.key());
store.addAll(TestData.getStocks());
root = new ContentPanel();
root.setHeadingText("Locked Grid Sample");
root.setPixelSize(600, 300);
final Resizable resizable = new Resizable(root, Dir.E, Dir.SE, Dir.S);
root.addExpandHandler(new ExpandHandler() {
@Override
public void onExpand(ExpandEvent event) {
resizable.setEnabled(true);
}
});
root.addCollapseHandler(new CollapseHandler() {
@Override
public void onCollapse(CollapseEvent event) {
resizable.setEnabled(false);
}
});
//locked grid
final Grid<Stock> lockedGrid = new Grid<Stock>(store, lockedCm) {
@Override
protected Size adjustSize(Size size) {
//this is a tricky part - convince the grid to draw just slightly too wide
//and so push the scrollbar out of sight
return new Size(size.getWidth() + XDOM.getScrollBarWidth() - 1, size.getHeight());
}
};
lockedGrid.setView(new GridView<Stock>(){{
this.scrollOffset=0;
}});
//require columns to always fit, preventing scrollbar
lockedGrid.getView().setForceFit(true);
//main grid, with horiz scrollbar
final Grid<Stock> grid = new Grid<Stock>(store, cm);
//don't want this feature, want to encourage horizontal scrollbars
// grid.getView().setAutoExpandColumn(nameCol);
grid.getView().setStripeRows(true);
grid.getView().setColumnLines(true);
grid.setBorders(false);
grid.setColumnReordering(true);
grid.setStateful(true);
grid.setStateId("gridExample");
//link scrolling
lockedGrid.addBodyScrollHandler(new BodyScrollHandler() {
@Override
public void onBodyScroll(BodyScrollEvent event) {
grid.getView().getScroller().scrollTo(ScrollDirection.TOP, event.getScrollTop());
}
});
grid.addBodyScrollHandler(new BodyScrollHandler() {
@Override
public void onBodyScroll(BodyScrollEvent event) {
lockedGrid.getView().getScroller().scrollTo(ScrollDirection.TOP, event.getScrollTop());
}
});
HorizontalLayoutContainer gridWrapper = new HorizontalLayoutContainer();
root.setWidget(gridWrapper);
//add locked column, only 300px wide (in this example, use layouts to change how this works
HorizontalLayoutData lockedColumnLayoutData = new HorizontalLayoutData(300, 1.0);
//this is optional - without this, you get a little offset issue at the very bottom of the non-locked grid
lockedColumnLayoutData.setMargins(new Margins(0, 0, XDOM.getScrollBarWidth(), 0));
gridWrapper.add(lockedGrid, lockedColumnLayoutData);
//add non-locked section, taking up all remaining width
gridWrapper.add(grid, new HorizontalLayoutData(1.0, 1.0));
}
return root;
}
@Override
public void onModuleLoad() {
RootPanel.get().add(asWidget());
}
}
There are a handful of issues (no line between locked and unlocked column, locked column header menu context icon is slightly out of place), but it covers most of the details without much hassle, and leaves almost all of it open to configuration - want the lock at the end? Just move the modifications around - want more than one locked column? just add more to the lockedCm.