So I am creating a treeview selector with C#/GTKSharp. I have the basic tree view selector functionality working: The data is loaded into my model and I can click on a node to collapse/expand.
The part I can't work out is how to tell the cell renderer to display the collapse/expand toggle button. In the examples it appears as a triangle that points right or down depending on whether the node is opened or collapsed. I just have a blank space that works as expected as I click but shows nothing.
One possibility is that I have a white on white text issue but I doubt it as my labels show up fine and I have not done any formatting yet.
I tried adding code for ShowExpanders but that was already true.
TreeView = new Gtk.TreeView();
// We add the event handlers (i.e. the control part) to the tree
TreeView.RowActivated += SelectorActivated; //On double click
TreeView.Selection.Changed += SelectorSelected; // On select (single click)
// Raise a context menu here??
// Connect to the ButtonPressEvent
// Raise a popup button
// Create columns [View]
Gtk.TreeViewColumn TreeViewColumTitle = new Gtk.TreeViewColumn();
TreeViewColumTitle.Title = "Profile";
Gtk.CellRendererText NameCellTitle = new Gtk.CellRendererText();
TreeViewColumTitle.PackStart(NameCellTitle, true);
TreeViewColumTitle.SetCellDataFunc(NameCellTitle, new Gtk.TreeCellDataFunc(RenderTitle));
NameCellTitle.Mode = CellRendererMode.Activatable;
// Populate the model
// Note that we could dispense with this step if we generated an ITreeModel
// interface in the Object class.
BindModel(Model);
// Attach everything to the pane
TreeView.Model = GTKModel;
TreeView.AppendColumn(TreeViewColumTitle);
TreeView.ShowExpanders = true;
TreeView.ExpanderColumn.Visible = true;
...
private void BindModel(Model Model) {
GTKModel = new Gtk.TreeStore(typeof(Object));
foreach (Object Object in Model.Selector) {
var BindingData = new BindingDataGTK(this, Object);
BindingData.Iter = GTKModel.AppendValues(Object);
Object.BindingData = BindingData;
BindChildren(GTKModel, BindingData);
}
}
private void BindChildren(TreeStore TreeStore, BindingDataGTK ObjectBinding) {
foreach (var Child in ObjectBinding.Object) {
var BindingData = new BindingDataGTK(this, Child);
BindingData.Iter = TreeStore.AppendValues(ObjectBinding.Iter, Child);
Child.BindingData = BindingData;
BindChildren(TreeStore, BindingData);
}
}
private void RenderTitle(Gtk.TreeViewColumn Column, Gtk.CellRenderer Cell,
Gtk.ITreeModel GTKModel, Gtk.TreeIter Iter) {
Object Object = (Object)GTKModel.GetValue(Iter, 0);
(Cell as Gtk.CellRendererText).Text = Object.Title;
Console.WriteLine("Render {0}", Object.Title);
}
So far as I know this is pretty much an automatic feature, I don't think anything special is needed to make it happen (I've certainly never needed to). You might want to try using a TreeIter to construct your tree instead?
E.g. assuming you already have a TreeView on your form with 0 (zero) columns in it called "treeview" and a list of "MyObject"s called "myListOfObjects"...
treeview.AppendColumn ("Some Title", new CellRendererText(), "text", 0);
Gtk.TreeStore _ts = new TreeStore (typeof(string));
foreach (IMyObject _mo in myListOfObjects) {
Gtk.TreeIter _it = _ts.AppendValues (_mo.SomeText);
RecurseInto (_ts, _it, _mo);
}
treeview.Model = _ts;
...
void RescureInto(Gtk.TreeStore ts, Gtk.TreeIter it, IMyObject mo)
{
foreach (IMyObject _child_mo in mo.Children) {
Gtk.TreeIter _it = ts.AppendValues (it, _child_mo.SomeText);
RecurseInto (ts, _it, _child_mo);
}
}
In theory this should work fine.
Related
Windows UWP app in C#. I have a method that checks a condition and depending on the condition, it may need to show a listview to the user so they can select an item from the list. I have more code in the method, after I potentially show the list view that needs to run after. However, because the listview shows and I have to wait for the SelectionChanged event handler to fire, I cannot figure out how to pause the calling method on that line until the event handler is completed for SelectionChanged. I don't have code written yet, so here is some pseduo code to illustrate:
private void LookupEmployee(string searchCriteria)
{
List<string> matches = GetEmployeeNameMatchesFromCriteria(searchCriteria);
if(matches.Count == null || matches.Count == 0)
{
//No matches found, warn user
return;
}
if(matches.Count == 1)
{
//We are good, we have just one match which is desirable.
}
if(matches.Count > 1)
{
//This means we have more than one match and we need to popup list view to have user select one
ShowListView(matches);
}
//Process Employee data here.
}
I know one option is to "daisy chain" calls by breaking out the final processing of employee data to another method and call that from the event handler for SelectionChanged of the listview. However, this has two issues. First, if I just have one match, then I will not be showing the listview or getting the SelectionChanged anyway. Second, if I had a bunch of variables and other things at the beginning of the method to be used at the end of the method, I don't want to (nor do I know how to) pass all of that through and back from the event handler in the event I need to show it.
I guess what I am looking for in a way is how the MessageDialog is handled.
var md = new MessageDialog("My Message");
md.Commands.Add(new UICommand("Okay")
{
});
var result = await md.ShowAsync();
if (result.Label == "Okay")
{
DoStuff;
}
Where using this will wait on the line:
await md.ShowAsync();
Until the user clicks the button, at which point the method can continue from there.
I guess I am looking for something similar to that where I can hold on the line of the method in the case that I need to show the listview until the user selects and item and grab the item that was selected.
Thoughts?
Thanks!
Okay, I think I found what I was looking for so I wanted to post the code. This is similar to how a modal window worked in the old days. Basically, you can use a ContentDialog which will allow you to "wrap" any controls you want in it. In my case, I want to show a ListView, so I wrap that in the ContentDialog. Here is what I have:
First we can do our tests and based on the tests, we can create the ContentDialog/ListView if needed. If we do create the ContentDialog, we can also setup the Display parameters so it fits the way we want it to.
private async void checkProductMatches()
{
var selectedItem = string.Empty;
//Check our results from DB.
if (productResults.Count == 0)
{
//This means we didn't find any matches, show message dialog
}
if (productResults.Count == 1)
{
//We found one match, this is ideal. Continue processing.
selectedItem = productResults.FirstOrDefault().Name;
}
if (productResults.Count > 1)
{
//Multiple matches, need to show ListView so they can select one.
var myList = new ListView
{
ItemTemplate = Create(),
ItemsSource =
productResults,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch
};
var bounds = Window.Current.Bounds;
var height = bounds.Height;
var scroll = new ScrollViewer() { HorizontalAlignment = HorizontalAlignment.Stretch, VerticalAlignment = VerticalAlignment.Stretch, Height = height - 100 };
var grid = new StackPanel();
grid.Children.Add(myList);
scroll.Content = grid;
var dialog = new ContentDialog { Title = "Title", Content = scroll };
Now, we wire up the event handler for the ListView SelectionChanged event and grab the selectedItem should this event raise.
myList.SelectionChanged += delegate (object o, SelectionChangedEventArgs args)
{
if (args.AddedItems.Count > 0)
{
MyProducts selection = args.AddedItems[0] as MyProducts;
if (selection != null)
{
selectedItem = selection.Name;
}
}
dialog.Hide();
};
Finally, we await the showing of the ContentDialog.
var s = await dialog.ShowAsync();
What this will do is, if we have one item, there is no need to popup the content dialog. So, we can assign the one result to our selectedItem variable and proceed. However, if we have multiple matches, we want to display a list for the user to select the one item. In this case, we create the ContentDialog, ListView and display parameters. They key is to wire up the event handler before we call to show the dialog and inside of the event handler, we make sure to cancel or close the dialog. Then we call to await the dialog showing. This will pause execution of this method on that line while the dialog is showing. Once the user selects an item, the event handler will raise, get the selected item and then close the dialog, which will then allow the method to continue execution from the awaited line.
Here is the full method:
private async void checkProductMatches()
{
var selectedItem = string.Empty;
//Check our results from DB.
if (productResults.Count == 0)
{
//This means we didn't find any matches, show message dialog
}
if (productResults.Count == 1)
{
//We found one match, this is ideal. Continue processing.
selectedItem = productResults.FirstOrDefault().Name;
}
if (productResults.Count > 1)
{
//Multiple matches, need to show ListView so they can select one.
var myList = new ListView
{
ItemTemplate = Create(),
ItemsSource =
productResults,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch
};
var bounds = Window.Current.Bounds;
var height = bounds.Height;
var scroll = new ScrollViewer() { HorizontalAlignment = HorizontalAlignment.Stretch, VerticalAlignment = VerticalAlignment.Stretch, Height = height - 100 };
var grid = new StackPanel();
grid.Children.Add(myList);
scroll.Content = grid;
var dialog = new ContentDialog { Title = "Title", Content = scroll };
myList.SelectionChanged += delegate (object o, SelectionChangedEventArgs args)
{
if (args.AddedItems.Count > 0)
{
MyProducts selection = args.AddedItems[0] as MyProducts;
if (selection != null)
{
selectedItem = selection.Name;
}
}
dialog.Hide();
};
var s = await dialog.ShowAsync();
}
//Test furter execution. Ideally, selected item will either be the one record or we will
//get here after the list view allows user to select one.
var stringTest = string.Format("Selected Item: {0}", selectedItem);
}
Hope this helps someone.
It's probably a very basic question about the behaviour of C# and WebControl. I got this working, but it would be nice if someone could clarify where the difference lays.
Before
I have a dictionary with a given key (Guid) and a Panel.
var tmpFormButtonPanel = new Panel();
_formButtonPanelDict.TryGetValue(new Guid(_hiddenField.Value), out tmpFormButtonPanel);
This panel contains a WebControl. Now I'd like to assign this button to another panel.
if (tmpFormButtonPanel != null)
{
var tmpControls = new List<Button>();
foreach (Button tmpButton in tmpFormButtonPanel.Controls)
{
tmpControls.Add(tmpButton);
}
tmpControls.Reverse();
foreach (var tmpButton in tmpControls)
{
tmpButton.AddCssClass("xy");
_buttonPanel.Controls.Add(tmpButton);
}
}
The moment I add the button to the _buttonPanel, it deletes the button out of tmpFormButtonPanel. From what I've heard or read, a WebControl can only be assigned to one panel. So this would explain why it doesn't work.
So I changed the code to this.
var tmpFormButtonList = new List<ButtonBaseUc>();
if (!_formButtonDict.TryGetValue(new Guid(_hiddenField.Value), out tmpFormButtonList))
{
tmpFormButtonList = new List<ButtonBaseUc>();
_formButtonDict.Add(new Guid(_hiddenField.Value), tmpFormButtonList);
}
foreach (var tmpButton in tmpFormButtonPanel.Controls)
{
if (tmpButton is ButtonBaseUc)
{
tmpFormButtonList.Add((ButtonBaseUc)tmpButton);
}
}
The last part does the same thing, but on the tmpFormButtonList.
if (tmpFormButtonList!= null)
{
var tmpControls = new List<Button>();
foreach (Button tmpButton in tmpFormButtonList)
{
tmpControls.Add(tmpButton);
}
tmpControls.Reverse();
foreach (var tmpButton in tmpControls)
{
tmpButton.AddCssClass("xy");
_buttonPanel.Controls.Add(tmpButton);
}
}
This is working. But why? I am only assigning the button to another list before adding it to the new panel. The references are still the same. What am I missing?
A control can only belong to one parent control. Since you have assigned it to the Panel in the dictionary-value, it will be removed there if you move it to the _buttonPanel.
This isn't documented but you can see it in the source:
// ...
if (control._parent != null) {
control._parent.Controls.Remove(control);
}
You have fixed this by not using a Panel as "storage" but a List<ButtonBaseUc>. This list is not a control(so the control has no parent), hence it must not be removed if you assign it to another (parent-)control.
I have amended the TODO list app to use a badge element instead of the boolean element as follows:
protected void PopulateTable()
{
tasks = TaskManager.GetTasks().ToList ();
UIImage ticked = new UIImage("checkbox_checked.png");
UIImage unticked = UIImage.FromFile("checkbox_unchecked.png");
Root = new RootElement("Tasky") {
new Section() {
from t in tasks
select (Element) new BadgeElement(t.Completed ? ticked : unticked, (t.Name==""?"<new task>":t.Name), delegate {
Console.WriteLine("???");
})
}
};
}
Is it possible to check to see if the user has clicked an icon rather than the text, and change the behaviour? Essentially I want to do this...
var task = tasks[indexPath.Row];
if(clickedIcon) {
currentTask = task;
task.Completed = !task.Completed;
TaskManager.SaveTask(currentTask);
} else {
ShowTaskDetails(task);
}
But I don't see any parameters inside IndexPath that allow me to access the column or the tapped element.
Any ideas
You need to create a custom version of the BadgeElement, and basically raise an event for the image that is separate from raising an event for the text.
Luckily for you, the whole source code is available, so you can just copy/paste BadgeElement, rename it, create a new unique key and modify it.
I'm displaying a set of search results in a ListView. The first column holds the search term, and the second shows the number of matches.
There are tens of thousands of rows, so the ListView is in virtual mode.
I'd like to change this so that the second column shows the matches as hyperlinks, in the same way as a LinkLabel shows links; when the user clicks on the link, I'd like to receive an event that will let me open up the match elsewhere in our application.
Is this possible, and if so, how?
EDIT: I don't think I've been sufficiently clear - I want multiple hyperlinks in a single column, just as it is possible to have multiple hyperlinks in a single LinkLabel.
You can easily fake it. Ensure that the list view items you add have UseItemStyleForSubItems = false so that you can set the sub-item's ForeColor to blue. Implement the MouseMove event so you can underline the "link" and change the cursor. For example:
ListViewItem.ListViewSubItem mSelected;
private void listView1_MouseMove(object sender, MouseEventArgs e) {
var info = listView1.HitTest(e.Location);
if (info.SubItem == mSelected) return;
if (mSelected != null) mSelected.Font = listView1.Font;
mSelected = null;
listView1.Cursor = Cursors.Default;
if (info.SubItem != null && info.Item.SubItems[1] == info.SubItem) {
info.SubItem.Font = new Font(info.SubItem.Font, FontStyle.Underline);
listView1.Cursor = Cursors.Hand;
mSelected = info.SubItem;
}
}
Note that this snippet checks if the 2nd column is hovered, tweak as needed.
Use ObjectListView -- an open source wrapper around a standard ListView. It supports links directly:
This recipe documents the (very simple) process and how you can customise it.
The other answers here are great, but if you don't want to have to hack some code together, look at the DataGridView control which has support for LinkLabel equivalent columns.
Using this control, you get all the functionality of the details view in a ListView, but with more customisation per row.
You can by inheriting the ListView control override the method OnDrawSubItem.
Here is a VERY simple example of how you might do:
public class MyListView : ListView
{
private Brush m_brush;
private Pen m_pen;
public MyListView()
{
this.OwnerDraw = true;
m_brush = new SolidBrush(Color.Blue);
m_pen = new Pen(m_brush)
}
protected override void OnDrawColumnHeader(DrawListViewColumnHeaderEventArgs e)
{
e.DrawDefault = true;
}
protected override void OnDrawSubItem(DrawListViewSubItemEventArgs e)
{
if (e.ColumnIndex != 1) {
e.DrawDefault = true;
return;
}
// Draw the item's background.
e.DrawBackground();
var textSize = e.Graphics.MeasureString(e.SubItem.Text, e.SubItem.Font);
var textY = e.Bounds.Y + ((e.Bounds.Height - textSize.Height) / 2);
int textX = e.SubItem.Bounds.Location.X;
var lineY = textY + textSize.Height;
// Do the drawing of the underlined text.
e.Graphics.DrawString(e.SubItem.Text, e.SubItem.Font, m_brush, textX, textY);
e.Graphics.DrawLine(m_pen, textX, lineY, textX + textSize.Width, lineY);
}
}
You can set HotTracking to true so that when the user hovers mouse over the item it appears as link.
i wish to create a form at runtime that will read the columns for any datasource and create fields based on the columns and datatype just like a datagridviews insert line
Best regards,
Mark
What you are doing sounds a lot like how PropertyGrid already works, which is essentially:
foreach(PropertyDescriptor prop in TypeDescriptor.GetProperties(obj)) {
object val = prop.GetValue(obj);
string s = prop.Converter.ConvertToString(val);
Control cont = // TODO: create some control and set x/y
cont.Text = s;
this.Controls.Add(cont);
}
To avoid lots of work with alignment, using Dock to set the positions might help:
using(Form form = new Form())
using (PropertyGrid grid = new PropertyGrid())
{
form.Text = obj.ToString(); // why not...
grid.Dock = DockStyle.Fill;
form.Controls.Add(grid);
grid.SelectedObject = obj;
form.ShowDialog(this);
}
I wonder if it is easier to use PropertyGrid in simple circumstances, though. Or there are some 3rd-party versions that work similarly.
Ok so heres what i came up with!
public partial class Form2 : Form
{
private Boolean isBrowsable(PropertyInfo info)
{
return info.GetCustomAttributes(typeof(BrowsableAttribute), false).Length>-1;
}
public Form2()
{
InitializeComponent();
}
public Form2(Boolean showCheckBoxes)
{
InitializeComponent();
_showCheckBoxes = true;
}
private Boolean _showCheckBoxes;
private Object _reflection;
private TableLayoutPanel _table = new TableLayoutPanel{Dock=DockStyle.Fill, CellBorderStyle = TableLayoutPanelCellBorderStyle.Single};
public Object SelectedObject
{
get
{
return _reflection;
}
set
{
//clear all controls from the table
_table.Controls.Clear();
foreach (var property in _reflection.GetType().GetProperties())
{
if (isBrowsable(property))
{
if ((property.PropertyType == typeof(int)) || (property.PropertyType == typeof(string)))
{
var textField = new TextBox { Dock = DockStyle.Fill, AutoSize = true };
textField.DataBindings.Add("Text", _reflection, property.Name);
_table.Controls.Add(textField, 2, _table.RowCount += 1);
var propertyLabel = new Label
{
Text = property.Name,
Dock = DockStyle.Fill,
TextAlign = ContentAlignment.MiddleLeft
};
_table.Controls.Add(propertyLabel, 1, _table.RowCount);
if (_showCheckBoxes)
{
var checkBox = new CheckBox
{
AutoSize = true,
Name = property.Name,
Dock = DockStyle.Left,
CheckAlign = ContentAlignment.TopLeft
};
_table.Controls.Add(checkBox, 0, _table.RowCount);
}
}
}
}
//add one extra row to finish alignment
var panel = new Panel { AutoSize = true };
_table.Controls.Add(panel, 2, _table.RowCount += 1);
_table.Controls.Add(panel, 1, _table.RowCount);
if (_showCheckBoxes)
{
_table.Controls.Add(panel, 0, _table.RowCount);
}
Controls.Add(_table);
if (!Controls.Contains(_table))
Controls.Add(_table);
}
}
public Boolean Execute(Object reflection)
{
SelectedObject = reflection;
return ShowDialog() == DialogResult.OK;
}
}
thanks all!
I don't fully understand your question. Is it correct that you want to create a Windows form which provides input fields (textboxes, checkboxes, etc.) for all fields/properties of an object that you feed to the form as its DataSource?
You might have to use reflection for this (see the System.Reflection namespace). For example, to get a list of all properties:
using System.Reflection;
....
public object DataSource;
...
Debug.Assert( DataSource != null );
var properties = DataSource.GetType().GetProperties();
You would then instantiate one input control per property:
foreach ( var property in properties )
{
// extract some information about each property:
string propertyName = property.Name;
Type propertyType = property.PropertyType;
bool propertyReadOnly = !property.CanWrite;
// create input controls based on this information:
// ...
}
However, it might be fairly tricky to reliably map property types to the correct input control; for example, what are you going to do when you encounter a property with some unknown class as its type, or when a property is a collection of values? You might have to create a sub-form inside your form in some cases; in other cases, a listbox might be enough.
I've recently built a sample project that uses the Dynamic Data assemblies of ASP.NET to do just this for a WPF grid, but I'm sure you could adapt the concept to WinForms. Dynamic Data provides much richer metadata than just reflection or the database, but it does require an Entity Data Model, or a LINQ to SQL data model.
basically, all you need is a reference to System.Web.DymamicData, and maybe you can find something useful in my class:
public class DynamicDataGridBuilder<TContext, TEntity> where TEntity : EntityObject
{
readonly MetaModel model = new MetaModel();
public DynamicDataGridBuilder()
{
model.RegisterContext(typeof(TContext), new ContextConfiguration { ScaffoldAllTables = true });
}
public void BuildColumns(DataGrid targetGrid)
{
MetaTable metaTable = model.GetTable(typeof(TEntity));
// Decision whether to auto-generated columns still rests with the caller.
targetGrid.Columns.Clear();
foreach (var metaColumn in metaTable.Columns.Where(x => x.GetType().Name == "MetaColumn" && x.Scaffold))
{
switch (metaColumn.ColumnType.Name)
{
case "Boolean":
targetGrid.Columns.Add(new DataGridCheckBoxColumn { Binding = new Binding(metaColumn.Name), Header = metaColumn.DisplayName });
break;
default:
targetGrid.Columns.Add(new DynamicDataGridTextColumn { MetaColumn = metaColumn, Binding = new Binding(metaColumn.Name), Header = metaColumn.DisplayName });
break;
}
}
}
}
TContext is the type of your object model, and TEntity the type of the entity / class in that model your want to generate controls for.
Use control data binding. It will do all the work for you.