Im trying to apply Getx State Management to update my screen(maybe responsive?),
I have read official getx github documents. and this is what I tried.
1. set variables that I want make it responsive to .obs
<e.g 1>
List blueBan = List<String>.filled(5, champIcon, growable: false).obs;
List redBan = List<String>.filled(5, champIcon, growable: false).obs;
==================================================================================================
2. set Widget to Obx(()=> Widget) that I want update (update like using setState method)
<e.g 2>
Widget banContainer(List banList, String team, int n) {
return Container(
...
child: Obx(
() => DragTarget<String>(
onWillAccept: (value) {
banTemp = value;
return true;
*variable banList is reference of blueBan,redBan.
I'm using it like banContainer(blueBan, 'blue', 0)
↓ this is error message what I've got.
════════ Exception caught by widgets library ═══════════════════════════════════
The following message was thrown building Obx(has builder, dirty, state: _ObxState#1202f):
[Get] the improper use of a GetX has been detected.
You should only use GetX or Obx for the specific widget that will be updated.
If you are seeing this error, you probably did not insert any observable variables into GetX/Obx
or insert them outside the scope that GetX considers suitable for an update
(example: GetX => HeavyWidget => variableObservable).
If you need to update a parent widget and a child widget, wrap each one in an Obx/GetX.
The relevant error-causing widget was
Obx
lib\…\ui\BanPick.dart:230
When the exception was thrown, this was the stack
#0 RxInterface.notifyChildren
package:get/…/rx_core/rx_interface.dart:29
#1 _ObxState.build
package:get/…/rx_flutter/rx_obx_widget.dart:54
#2 StatefulElement.build
package:flutter/…/widgets/framework.dart:4691
#3 ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4574
#4 StatefulElement.performRebuild
package:flutter/…/widgets/framework.dart:4746
...
════════════════════════════════════════════════════════════════════════════════
Nevertheless I inserted .obs on my List variable, it says "If you are seeing this error, you probably did not insert any observable variables into GetX/Obx"
thanks for reading.
You have to use RxList for obserble then it will work in obx otherwise you have to use getbuilder and call the update() for ui changes
Related
It was mentioned in some of my other threads on my app on that my code was incorrect because the apply button is present. I understand that now. It was said to collect the controls and then when apply is hit to send the data.
I have now idea how to approach that. So for the sake of general education. I have 1 property sheet and 5 property pages. For the sake of just general controls in use. Between all the 5, there are only radio controls and buttons, no edit controls (yet).
Let's assume there are 2 radios buttons and 1 button on each page.. where page 1 is radio1, radio2, button 1 and page 2 is radio3, radio4, button2....and so on.
I know that when the user selects something like a button or radio that the IsModified(TRUE) needs to be called to enable the apply button from grayed to active.
What would the code look like to scan all the controls and then apply them? I've never done it and I can't seem to find an example that isn't already super busy to gain the understanding of how to do it.
Anyone have a tutorial or code snippet or build a primer from the controls that I described above that could demonstrate how to execute this?
Update:
Ok so I have the DDX variables added:
void CSettingsUserTabs::DoDataExchange(CDataExchange* pDX)
{
CMFCPropertyPage::DoDataExchange(pDX);
DDX_Control(pDX, STYLE_3D_USER, m_style_3d);
DDX_Control(pDX, STYLE_FLAT_USER, m_style_flat);
DDX_Control(pDX, STYLE_FLAT_SHARED_HORZ_SCROLL_USER, m_style_flat_shared_h_scroll);
DDX_Control(pDX, STYLE_3D_SCROLLED_USER, m_style_3d_scroll);
DDX_Control(pDX, STYLE_3D_ONENOTE_USER, m_style_onenote);
DDX_Control(pDX, STYLE_3D_VS2005_USER, m_style_vs2005);
DDX_Control(pDX, STYLE_3D_ROUNDED_USER, m_style_3d_rounded);
DDX_Control(pDX, STYLE_3D_ROUNDED_SCROLL_USER, m_style_3d_rounded_scroll);
}
My radio selection look like:
void CSettingsUserTabs::OnBnClicked3dUser()
{
//AfxGetMainWnd()->SendMessage(WM_COMMAND, STYLE_3D_USER);
UpdateData(TRUE);
}
void CSettingsUserTabs::OnBnClickedFlatUser()
{
// TODO: Add your control notification handler code here
//AfxGetMainWnd()->SendMessage(WM_COMMAND, STYLE_FLAT_USER);
UpdateData(TRUE);
}
..... and the rest of them....
So to me, when I click any radio button, I expect that it scans all of controls on that property page? If so, all the variables have the values..do I call IsModified(); to enable the apply button...which then executes all the radio values i.e. only one selected? Is that the flow?
Update 2:
So this is what my modal dialog code is in MainFrame.cpp:
void CMainFrame::OnSettingsTools()
{
SettingsSheet SettingsSheet(L"Application Settings");
CSettingsPowerUser pgePowerUser;
CSettingsToolbars pgeToolbars;
CSettingsTheme pgeTheme;
CSettingsUserTabs pgeUserTabs;
CSettingsReset pgeReset;
SettingsSheet.AddPage(&pgeToolbars);
SettingsSheet.AddPage(&pgeTheme);
SettingsSheet.AddPage(&pgeUserTabs);
SettingsSheet.AddPage(&pgePowerUser);
SettingsSheet.AddPage(&pgeReset);
INT_PTR nRet = -1;
nRet = SettingsSheet.DoModal();
// Handle the return value from DoModal
switch (nRet)
{
case -1:
AfxMessageBox(_T("Dialog box could not be created!"));
break;
case IDABORT:
// Do something
AfxMessageBox(_T("ABORT!"));
break;
case IDOK:
// Do something
OnUserTabStyles(1);
AfxMessageBox(_T("OK!"));
break;
case IDCANCEL:
// Do something
AfxMessageBox(_T("CANCEL"));
break;
default:
// Do something
break;
};
}
The routine to verify if any of the radios buttons are changed in SettingsTabs.cpp:
void CSettingsUserTabs::OnTabRadioClicked(UINT nCmdID)
{
BOOL IsChecked = nCmdID;
CheckRadioButton(STYLE_3D_USER, STYLE_3D_ROUNDED_SCROLL_USER, nCmdID);
UpdateData(TRUE);
m_tabCmdID = nCmdID;
SetModified();
}
What the member variables look like in SettingsUserTabs.cpp:
void CSettingsUserTabs::DoDataExchange(CDataExchange* pDX)
{
CMFCPropertyPage::DoDataExchange(pDX);
DDX_Radio(pDX, STYLE_3D_USER, m_style_3d);
DDX_Radio(pDX, STYLE_FLAT_USER, m_style_flat);
DDX_Radio(pDX, STYLE_FLAT_SHARED_HORZ_SCROLL_USER, m_style_flat_h_scroll);
DDX_Radio(pDX, STYLE_3D_SCROLLED_USER, m_style_3d_scroll);
DDX_Radio(pDX, STYLE_3D_ONENOTE_USER, m_style_3d_onenote);
DDX_Radio(pDX, STYLE_3D_VS2005_USER, m_style_vs2005);
DDX_Radio(pDX, STYLE_3D_ROUNDED_USER, m_style_3d_rounded);
DDX_Radio(pDX, STYLE_3D_ROUNDED_SCROLL_USER, m_style_3d_rounded_scroll);
}
What the constructor looks like in SettingsUserTabs.cpp:
CSettingsUserTabs::CSettingsUserTabs()
: CMFCPropertyPage(IDD_SETTINGS_TABS)
, m_style_3d(FALSE)
, m_style_flat(FALSE)
, m_style_flat_h_scroll(FALSE)
, m_style_3d_scroll(FALSE)
, m_style_3d_onenote(FALSE)
, m_style_vs2005(FALSE)
, m_style_3d_rounded(FALSE)
, m_style_3d_rounded_scroll(FALSE)
, m_tabCmdID(FALSE)
{
}
This issue I'm seeing now is when I try to use the member variable m_tabCmdID it is coming back to unknown identifier so I'm not sure why the member variable isn't be seen. I am was expecting to use it like OnUserTabStyles(m_tabCmdID); so that it would pass the argument of the selected button to the method OnUserTabStyles. For now I just dumped a 1 in there to see if the mechanism works. I just am not clear how to access the member variable from the SettingsUserTabs.cpp from the IDOK. What am I missing?
EDIT: The range of options are sequential in the resource.h as 200-207, that is something I'm aware of and I know many don't like range options as they can get corrupted...this is my code, so I have no worries about the range being messed with.
Update 3:
Ok, so I finally understand the mechanism that Constantine described with the help of:
https://helgeklein.com/blog/2009/10/radio-buttons-in-mfc-visual-studio-2008-c/
I didn't have the tab order right nor did I have the first control set to true for the group.
With that, I now get the values 0-7 mentioned in the button group when I debug as I click each radio button based on its position in the group from 0-7 i.e. 8 buttons. Here is what the code looks like now.
SettingsUserTabs.cpp:
CSettingsUserTabs::CSettingsUserTabs()
: CMFCPropertyPage(IDD_SETTINGS_TABS)
, m_style_tabs(FALSE)
{
}
void CSettingsUserTabs::DoDataExchange(CDataExchange* pDX)
{
CMFCPropertyPage::DoDataExchange(pDX);
DDX_Radio(pDX, STYLE_3D_USER, m_style_tabs);
}
void CSettingsUserTabs::OnTabRadioClicked(UINT nCmdID)
{
UpdateData(TRUE);
BOOL RadioValueSelected = m_style_tabs; // only here to see 0-7 value for debugging only, not needed, test only
SetModified();
}
The mainframe.cpp above (Update 2): void CMainFrame::OnSettingsTools() is still the same.
So now here is where my question still isn't clear, I call the domodal from mainframe.cpp, the member variable m_style_tabs is in SettingsUserTabs.cpp. When I try to access the member variable, it says unknown identifier when I try to do something like this after the domodal int temp = m_styles_tabs;. I have a this in the mainframe.cpp
void CMainFrame::DoDataExchange(CDataExchange* pDX)
{
// TODO: Add your specialized code here and/or call the base class
CMDIFrameWndEx::DoDataExchange(pDX);
}
I would expect that the member would be seen in mainframe.cpp so I can process it after the domodal which is what I thought the whole point of this is?
How do I access the member variable so I can do the OnApply to it? I think I almost got it, I am just unclear on how to execute the last few steps for the actual apply itself.
Update 4:
The reason I ask about the restart is when the user selects the tabs property page and choose 1 of 8 choices, that option is stored in the registry and read during the Oncreate and then uses that style. Since it is OnCreate I haven't found a way to "redraw" or use the new tab setting except to restart the app. So by doing the reg save > respawn > end old ...I would want the dialog to reopen at the same tab property page so the user can see what the changes are if the apply was selected...vs. some dialog that says "restart"..or whatever. Below is the code used to demonstrate how it is working now.
OutputWnd.cpp
int COutputWnd::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CDockablePane::OnCreate(lpCreateStruct) == -1)
return -1;
CRect rectDummy;
rectDummy.SetRectEmpty();
// Create User Define tab style:
int UserTabStyle = AfxGetApp()->GetProfileInt(_T("Settings"), _T("UserTabStyle"), 0); //Get value from registry
// If the key doesn't exist, UserTableStyle will be 0 or FALSE;
if (UserTabStyle != FALSE && UserTabStyle <= 8) { // User selected tab style type
int EnumUserTabStyle = UserTabStyle - 1; // Fix enum if key doesn't exist.
if (!m_wndTabs.Create(static_cast<CMFCTabCtrl::Style>(EnumUserTabStyle), rectDummy, this, 1))
{
TRACE0("Failed to create output tab window\n");
return -1; // fail to create
}
}
else { // Default tabs style if Reg key does not exist i.e. new install/program reset
if (!m_wndTabs.Create(CMFCTabCtrl::STYLE_FLAT, rectDummy, this, 1))
{
TRACE0("Failed to create output tab window\n");
return -1; // fail to create
}
}
… rest of function....
Update 5:
Here is the Apply in use from SettingsUserTabs.cpp:
BOOL CSettingsUserTabs::OnApply()
{
// TODO: Add your specialized code here and/or call the base class
AfxGetApp()->WriteProfileInt(_T("Settings"), _T("UserTabStyle"), m_style_tabs); // Save value to registry
return CMFCPropertyPage::OnApply();
}
Update 6:
Everything to this point is working, I ran into a road block trying to apply lessons learned here and at this link:
MFC MDI Substituting a class member dynamically
What was done in this topic was for the OutputWnd pane which works brilliantly! My MDI opens a file and uses CTabView and using the link shown in Update 6 allowed me to change the tabs on boot. Now that I have the OutputWnd doing it with OnApply, I'm trying to apply it to the document view when a file is loaded. I'm running into an access violation when I call the new function I created in TrainView.cpp and calling it from UserSettingsTabs.cpp. I thought it was the static_cast operation, but even if I do a simple bold using GetControlTabs() that also crashes (Shown in the commented out code, was on boot, now in OnApply to test theory). So clearly I need to capture the MDI document but not sure how that is done. I thought it would be as simple as:
GetTabControl().ModifyTabStyle(static_cast<CMFCTabCtrl::Style>(EnumUserTabStyle));
But when that crashed with a Cx000000005 access violation, I knew something was wrong on my end. I can't modify the CTabView operation, so I'm looking to see if we can fix what I'm doing wrong to have the OnApply change the tab styles without restarting as done in the OutputWnd we just fixed.
So as it stands, the OnApply that is working and now modded to try an integrate the CTabView functionality:
SettingsUserTabs.cpp:
BOOL CSettingsUserTabs::OnApply()
{
BOOL bResult = CMFCPropertyPage::OnApply();
if (bResult)
{
AfxGetApp()->WriteProfileInt(_T("Settings"), _T("UserTabStyle"), m_style_tabs); // Save value to registry
((CMainFrame*)AfxGetMainWnd())->m_wndOutput.m_wndTabs.ModifyTabStyle((CMFCTabCtrl::Style)m_style_tabs);
((CMainFrame*)AfxGetMainWnd())->m_wndOutput.m_wndTabs.RecalcLayout();
CTrainView* TrainTabs; // User Call from anywhere method
TrainTabs->TrainDocUpdateTabsControl();
}
return bResult;
}
I added the function CTrainView::TrainDocUpdateTabsControl() to update the tabs...the rest of the code is fully operation i.e. void CTrainView::OnInitialUpdate()
The TrainView.cpp:
IMPLEMENT_DYNCREATE(CTrainView, CTabView)
void CTrainView::OnInitialUpdate()
{
CMainFrame* pMainFrame = (CMainFrame*)AfxGetMainWnd();
pMainFrame->m_wndOutput.AddStringDebugTab(_T("Debug: TrainView--CTrainView::OnInitialUpdate()."));
// add views // cmb
AddView(RUNTIME_CLASS(CInformationView), AfxStringID(IDS_INFORMATION));
AddView(RUNTIME_CLASS(CChaptersView), AfxStringID(IDS_CHAPTERS));
// Nicely hack to access protected member
class CMFCTabCtrlEx : public CMFCTabCtrl
{
public:
void SetDisableScroll() { m_bScroll = FALSE; }
};
// One-Liner to Disable navigation control
((CMFCTabCtrlEx*)&GetTabControl())->SetDisableScroll();
GetTabControl().EnableTabSwap(TRUE);
GetTabControl().SetLocation(CMFCBaseTabCtrl::Location::LOCATION_BOTTOM);
//GetTabControl().SetActiveTabBoldFont(TRUE);
GetTabControl().EnableAutoColor(TRUE);
// Modify User Define tab style:
int UserTabStyle = AfxGetApp()->GetProfileInt(_T("Settings"), _T("UserTabStyle"), 0); //Get value from registry
// If the key doesn't exist, UserTableStyle will be 0 or FALSE;
if (UserTabStyle != FALSE && UserTabStyle <= 8) { // User selected tab style type
int EnumUserTabStyle = UserTabStyle - 1; // Fix enum if key doesn't exist.
GetTabControl().ModifyTabStyle(static_cast<CMFCTabCtrl::Style>(EnumUserTabStyle));
}
else { // Default tabs style if Reg key does not exist i.e. new install/program reset
GetTabControl().ModifyTabStyle(CMFCTabCtrl::STYLE_FLAT);
}
CTabView::OnInitialUpdate();
}
void CTrainView::TrainDocUpdateTabsControl()
{
CTabView::AssertValid();
GetTabControl().SetActiveTabBoldFont(TRUE); << CAUSES Cx000000005 ACCESS ERROR CRASH WHEN CALLED.
//int EnumUserTabStyle;
//int UserTabStyle = AfxGetApp()->GetProfileInt(_T("Settings"), _T("UserTabStyle"), 0); //Get value from registry
//((CMainFrame*)AfxGetMainWnd())->GetTabControl().ModifyTabStyle(static_cast<CMFCTabCtrl::Style>(EnumUserTabStyle));
}
Update 6 EDIT:
The tabview is created from Application.cpp like this:
//Load Train Template
m_pkDocTrainTemplate = new CMultiDocTemplate(
IDR_TRAIN, // Loads TRAIN operation
RUNTIME_CLASS(CTrainDoc),
RUNTIME_CLASS(CChildFrame),
RUNTIME_CLASS(CTrainView));
AddDocTemplate(m_pkDocTrainTemplate);
I tried to cast the m_pkDocTrainTemplate as I thought that was the pointer? Since it is MDI, I wasn't sure if there was an index issue since there can be multiple docs open simultaneously?
Below is an example of an application I wrote some time ago. It's a simple "Settings" dialog. Unlike yours, this one is derived from CDialogEx. But as CPropertyDialog is derived from CDialog, these apply in your case too.
Using the Wizard, I added member variables to the dialog class, bound to the dialog controls. Choose "Value", rather than "Control" in the "Category" combo in the Wizard. These are declared in the class definition. For simplicity, I only show three. There is a CString, an int and a BOOL variable, bound to an edit, a combo-box (drop-down list) and a check-box control respectively.
class CSettingsDlg : public CDialogEx
{
.
.
public:
CString m_DBConn;
int m_DumpSQL;
BOOL m_bLineNums;
}
In the implementation, the Wizard has modified the constructor and the DoDataExchange() member functions:
CSettingsDlg::CSettingsDlg(CWnd* pParent /*=NULL*/)
: CDialogEx(CSettingsDlg::IDD, pParent)
, m_DBConn(_T(""))
, m_DumpSQL(0)
, m_bLineNums(FALSE)
{
}
void CSettingsDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
DDX_Text(pDX, IDC_EDIT_DBCONN, m_DBConn);
DDV_MaxChars(pDX, m_DBConn, 255);
DDX_CBIndex(pDX, IDC_COMBO_DUMPSQL, m_DumpSQL);
DDV_MinMaxInt(pDX, m_DumpSQL, 0, 2);
DDX_Check(pDX, IDC_CHECK_LINENUMS, m_bLineNums);
}
The values in the constructor are the initial (default) ones. The DoDataExchange() function calls the DDX/DDV routines. The DDX routines perform the transfer of data (controls<->variables), while the DDV ones the validation - they are optional. The DoDataExchange() function is called by UpdateData(). Also, the default implementation of OnOK() calls UpdateData(TRUE), and if successful closes the dialog.
You will need to enable the Apply button if something has been modified. You can capture notification messages like EN_CHANGE, EN_UPDATE, BN_CLICKED etc (add an event in the property editor) and call the SetModified() function - this can prove quite tedious, but I can't see any other way to do it.
Such a dialog class can be used in the application as shown below:
void CChildView::OnSetoptions()
{
// Create a Settings-dialog class instance
CSettingsDlg sd; // Main application window as parent - will block every UI item in the application
// Set initial values for the member variables
sd.m_DBConn = szDBconn;
sd.m_DumpSQL = nDumpSQL;
sd.m_bLineNums = bDumpLineNums;
if (sd.DoModal() == IDOK)
{
// Store the values entered by the user
lstrcpyn(szDBconn, sd.m_DBConn, MAX_PATH);
nDumpSQL = sd.m_DumpSQL;
bDumpLineNums = sd.m_bLineNums;
}
}
EDIT :
I have a usage example in the code section just above. The procedure is create an instance of the dialog class, set the member variables' values (initial values, eg read from the registry), call DoModal() and if successful store (copy) the variables somewhere else (if not discard them). This should be done in some event handler, like CMainFrame::OnSettingsTools() in Update2. The CMainFrame::DoDataExchange() override makes no sense.
It's quite strange that you can't access the m_style_tabs variable. Isn't it a non-static, public member of the CSettingsUserTabs class? It should be declared in SettingsUserTabs.h. Won't it work if you acceess it as pgeUserTabs.m_style_tabs? In the CSettingsUserTabs class functions it can accessed simply as m_style_tabs. Also I see it is initialized as FALSE in the constructor. Is it a BOOL and not an int? (btw BOOL is defined as int in Win32, so the compiler won't complain) But the Wizard generates an int variable for radio-buttons, optionally with range validation as well.
Another point, you don't normally need to call UpdateData(TRUE) in CSettingsUserTabs::OnTabRadioClicked(). Please leave only the SetModified() call there. UpdateData(TRUE) is typically called in the OnOK() function. And usually you don't need to override these, because the default implementation is sufficient. The CPropertyPage documentation btw mentions that The default implementation of OnApply calls OnOK.
EDIT 2 :
In OnApply() you should first check if validation was successful. Also, OnCreate() isn't a "method" that can be called directly. It should be considered an "event". It's called by the framework when a window is created. You should instead call Create(). In your case you can destroy the Output Window and create it anew (with the new style). But, I see that the CMFCTabCtrl class has a ModifyTabStyle() function, which you can try calling, (without destroying the windows and creating it again). So, your code would become:
BOOL CSettingsUserTabs::OnApply()
{
BOOL bResult = CMFCPropertyPage::OnApply();
if (bResult)
{
AfxGetApp()->WriteProfileInt(_T("Settings"), _T("UserTabStyle"), m_style_tabs); // Save value to registry
((CMainFraime*)AfxGetMainWnd())->m_wndOutput.m_wndTabs.ModifyTabStyle((CMFCTabCtrl::Style)m_style_tabs);
}
return bResult;
}
The above code won't compile, because the m_wndOutput and m_wndTabs members are protected. You will have to make them public.
I have a form (which I'll call MainForm) embedded with a TabControl. Every time the user creates a new tab it is filled with an instance of a pre-built Panel (which I'll call MyPanel) which contains many controls.
My MyPanel class has a private variable bool save_state which is set to false every time one of the (editable) controls is edited and set to true when the user "saves" the state of the panel.
I want a visual flag to keep track of tabs that have unsaved changes (e.g. the tab "Tab1" will instead display the text "Tab1 *" if it has unsaved changes). So I want to set up the event handler in my MainForm which can call a method in MyPanel to add the handler to each control.
Since not all my controls use the same EventHandler type (for example, I also need to track DataGridViewRowsAddedEvent, among others), I currently have several methods adding the appropriate handler to the corresponding controls (one for each type of Event Handler), each of which is running the same code (i.e. set the save_state bit to false and append " *" to the tab text.
For example, in MainForm.cpp I have:
#include "MyPanel.h"
void markUnsaved(void) {
// set panel bit to false
// append " *" to tab text if we haven't already
}
void MainForm::handler1(Object ^sender, EventArgs ^e) {
markUnsaved();
}
void MainForm::handler2(Object ^sender, DataGridViewRowsAddedEventArgs ^e) {
markUnsaved();
}
void Main::FormaddNewPanelToTab(int tab_index) {
// check index is valid ...
// make the new panel
MyPanel ^new_panel = gcnew MyPanel();
new_panel->addEventHandlerToControls(gcnew EventHandler(this, &MainForm::handler1));
new_panel->addDgvEventHandlerToControls(gcnew DataGridViewRowsAddedEventHandler(this, &MainForm::handler2));
// rest of code...
}
Though this currently works as intended, this (along with the several other Event Handler types I have to manage) makes my code look really silly.
I am hoping to be able to have have a single event handler in MainForm and a single method in MyPanel which type-casts the Event Handler passed and adds it to all the controls with the appropriate types.
I have tried doing simple casts such as:
void MyPanel::addHandlerToControls(EventHandler ^handler) {
control_NUD->ValueChanged += handler; // this works because ValueChanged is of type EventHandler
control_DGV->RowsAdded += (DataGridViewRowsAddedEventHandler ^)handler; // this compiles but throws an exception
// rest of the code...
}
to no avail.
Any help would be greatly appreciated!
I know this is maybe a bit late for answer but I'd want to show how would I solve this.
Firs of all I suggest to get rid from idea of casting event handlers. Kind of such approach may work in C# (with some adjustments) but as far as I know it's not possible in C++ /CLI.
I'd go for adding new event to a MyPanel class that will be invoked every time when the data on a panel is changed. But to avoid adding a lot of different handlers to a control events in a MyPanel class it's better to create one generic method that will handle all the neccessary control's events and fire new event. Maybe this sounds messy, let me show the code:
public ref class MyPanel
{
// Add a new event
public:
event EventHandler^ DataChanged;
// Add a method that will fire new event
// this methid will be invoked on every control's event that you'll subscribe
private:
generic <typename T>
void DataChangedHandler(System::Object^ sender, T e)
{
// Fire the event
DataChanged(this, EventArgs::Empty);
}
// Once the controls are initialized you may add the event handlers
// I put it in a constructor only for example
MyPanel()
{
control_NUD->ValueChanged += gcnew EventHandler(this, &MyPanel::DataChangedHandler<EventArgs^>);
control_DGV->RowsAdded += gcnew DataGridViewRowsAddedEventHandler(this, &MyPanel::DataChangedHandler<DataGridViewRowsAddedEventArgs^>);
// and so on...
}
}
/// And now in a main form we only need to subscribe to a DataChanged event
public ref class MainForm
{
//...
// the handler
void MyHandler(Object^ sender, EventArgs^ e)
{
markUnsaved();
}
void FormaddNewPanelToTab(int tab_index)
{
// make the new panel
MyPanel ^new_panel = gcnew MyPanel();
new_panel->DataChanged += gcnew EventHandler(this, &MainForm::MyHandler);
}
//...
}
Hope this helps.
I'm displaying data in a GridView from a custom QAbstractListItem subclass (implementation here). Adding and removing items works fine, QML is notified of the changes and transitions work fine.
Now I'm trying to set a property of an Item inside the model and have QML react on it. The problem is, the onPropertyChanged inside QML is not called.
Here is the property in C++:
// item.h
Q_PROPERTY(bool pToBeDeleted READ toBeDeleted NOTIFY toBeDeletedChanged)
// item.cpp
void Item::requestDelete()
{
toBeDeleted_m = true;
qDebug() << "emitting";
emit toBeDeletedChanged();
}
This is what the GridView looks like:
// main.qml
GridView {
id: grid
// ...
model: empty
delegate: customComponent {
toBeDeleted: pToBeDeleted
}
ListModel {
id: empty
}
}
When the program starts, grid's model is set to my itemmodel.
And this is the QML type that does not see the changes:
// customComponentForm.ui.qml
Item {
property bool toBeDeleted: false
}
// customComponent.qml
CustomComponentForm {
onToBeDeletedChanged: {
console.debug("change")
}
}
Now when I call the method from inside the model like this:
this->items.at(i++)->requestDelete();
The output shows emitting but not change.
I have tried to include
emit dataChanged(createIndex(i, 0), createIndex(i, 0));
which did result in onToBeDeletedChanged to be called sometimes, but that also resulted in some wonky behaviour with the error
DelegateModel::item: index out range 3 3
Two things went wrong here. First, because of the ++ at
this->items.at(i++)->requestDelete();
the dataChanged emit had the wrong index which resulted in wrong items being updated. Second of all,
emit dataChanged(createIndex(i, 0), createIndex(i, 0));
was missing the third argument, and since in another attempt I had tried inline defining of a Vector the wrong way, I didn't find this to be the problem right away. The right call here would have been
QVector<int> v;
v.append(Qt::UserRole + 7 + 1);
// pToBeDeleted being the 7th property, always check this with
// roleNames()[Qt::UserRole + i + 1]. It should say your property.
emit dataChanged(createIndex(i, 0), createIndex(i, 0), v);
My mistake.
But on another note, since the rolename index seems to be platform dependent and signaling the change from the model is somewhat of a dirty approach, a better solution (as suggested by Kevin Krammer) would be to rewrite the itemmodel to only contain a single property, which is the QObject item. That way QML is notified of the changes item's properties have.
I have a QML file containing this:
Text {
id: testData
onTaskClicked:{
testData.text = task.name
}
}
The catch is this taskClicked signal. It is emitted by another widget (C++) and needs to be relayed to QML.
This is similar to this SO question, except that the solution posted there doesn't work (why is written below).
The C++ code:
ctxt->setContextProperty(QLatin1Literal("holiday"), m_model);
ctxt->setContextProperty(QLatin1Literal("bgcolor"), color);
view->setResizeMode(QQuickView::SizeRootObjectToView);
auto mainPath = QStandardPaths::locate(QStandardPaths::DataLocation,
QLatin1Literal("taskview.qml"));
view->setSource(QUrl::fromLocalFile(mainPath));
ctxt->setContextProperty(QLatin1Literal("viewer"), m_view);
m_view is a QListView subclass that emits the taskClicked(HolidayTask* task) signal (from the .h file):
Q_SIGNALS:
void taskClicked(HolidayTask* task);
color and m_model are registered in QML and are used elsewhere. The object from the signal is already registered in QML. view is my QQuickView.
First I tried the solution presented in the question above:
auto root = view->rootObject();
auto myElement = root->findChild<QObject*>(QLatin1Literal("testData");
connect(m_view, SIGNAL(taskClicked(HolidayTask* task), myElement,
SLOT(taskClicked(HolidayTask* task);
However, myElement is always null (and I get a runtime warning about a non existent slot).
If I try to set the view (the QListView) pointer as a context property of the QML view, it still doesn't work.
In all cases, I get also:
QML Connections: Cannot assign to non-existent property "onTaskClicked"
What could I possibly be doing wrong here?
EDIT to clarify some details: HolidayTask is a custom QObject subclass, and the signal taskClicked is defined in C++ (in a QListView subclass)
EDIT2: We're getting close, but no cigar:
auto root = quickView->rootObject();
auto myElement = root->findChild<QObject*>(QLatin1Literal("testData"));
connect(m_view, SIGNAL(taskClicked(HolidayTask*)),
myElement, SIGNAL(taskClicked(HolidayTask* task)));
and
Text {
id: testData
objectName: "testData"
signal taskClicked(HolidayTask task)
onTaskClicked: {
testData.text = task.name
console.log("CLICk!")
}
}
yields
QObject::connect: No such signal QQuickText_QML_0::taskClicked(HolidayTask* task) in /home/lb/Coding/cpp/holiday-planner/src/mainwindow.cpp:178
QObject::connect: (receiver name: 'testData')
More details: HolidayTask, my custom QObject subclass, is registered in the code as
qmlRegisterType<HolidayTask>("HolidayPlanner", 1, 0, "HolidayTask");
Minimal QML with the data:
import QtQuick 2.0
import QtQml 2.2
import HolidayPlanner 1.0
Rectangle {
id: container
objectName: "container"
color: bgcolor
Text {
id: testData
objectName: "testData"
signal taskClicked(HolidayTask task)
onTaskClicked: {
testData.text = task.name
console.log("CLICK")
}
}
}
EDIT3: The final, working code is (see answers on why)
connect(m_view, SIGNAL(taskClicked(HolidayPlanner::HolidayTask*)),
myElement, SIGNAL(taskClicked(HolidayPlanner::HolidayTask*)));
This worked only through the use of objects with full namespaces. Otherwise the signature will not match in QML.
However, myElement is always null (and I get a runtime warning about
a non existent slot).
You are trying to find the child based on id, whereas it is based on the objectName property. You would need to set the objectName property to the desired to actually find it.
Also, you do not seem to declare the signal in your QML Text item. I am not sure if it is a custom C++ item or a built-in one. You have not shared enough code unfortunately to understand that bit. Either way, declare your signal as per documentation.
Therefore, try this code out:
Text {
id: testData
objectName: "testData"
// ^^^^^^^^^^^^^^^^^^^
signal taskClicked (HolidayTask task)
// ^^^^^^^^^^^^^^^^^^^
onTaskClicked:{
testData.text = task.name
}
}
Once that is done, you are almost ready. You need to have your HolidayTask registered to QML for sure, and you also need to change the connect syntax in your main.cpp as follows:
connect(m_view, SIGNAL(taskClicked(HolidayTask* task), myElement, SIGNAL(taskClicked(HolidayTask* task)));
In short, you need to trigger your QML signal handler this way, and not via SLOT.
Also, note that your connect syntax is broken as it is missing the closing brackets at the end. That needs to be fixed.
I would even consider removing the pointer assignment and use value or reference based passing for this.
You can connect a signal from C++ to QML by:
view->rootContext()->setContextProperty("testData",this);
QObject::connect(this,SIGNAL(taskClicked(HolidayTask* task)),(QObject *)view->rootObject(),SLOT(onTaskClicked(HolidayTask* task)));
When your signal is named taskClicked, The slot in QML should be onTaskClicked.
Also in QML you should name the object testData by:
objectName: "testData"
QML Connections: Cannot assign to non-existent property "onTaskClicked"
The error tells you that your Text item do not have signal taskClicked or property onTaskClicked!
You need to declare a slot inside your text item. To do that, you simply declare a function:
Text {
id: testData
objectName: "testData" // as Laszlo said
function onTaskClicked( task ) {
testData.text = task.name;
}
}
But that also won't work because you create a SLOT( onTaskClicked(QVariant) ) instead of SLOT(taskClicked(HolidayTask*)). In order to exchange data with QML you need to change your signal to SIGNAL(taskClicked(QVariant)):
Q_SIGNALS:
void taskClicked(QVariant task);
And emit it with:
emit taskClicked( QVariant::fromValue( task ) );
Remember that in order to be able to use HolidayTask it must be a QObject that is registered with qmlRegisterType.
You can also simply call that qml function.
If you are not able to use QVariant you can declare a signal inside you Text object:
Text {
id: testData
objectName: "testData" // as Laszlo said
signal taskClicked ( HolidayTask task )
onTaskClicked: {
testData.text = task.name;
}
}
And then connect from a C++ SIGNAL to qml SIGNAL:
auto root = view->rootObject();
auto myElement = root->findChild<QObject*>(QLatin1Literal("testData");
connect(m_view, SIGNAL(taskClicked(HolidayTask*), myElement,
SIGNAL(taskClicked(HolidayTask*));
I have a QML file containing this:
Text {
id: testData
onTaskClicked:{
testData.text = task.name
}
}
The catch is this taskClicked signal. It is emitted by another widget (C++) and needs to be relayed to QML.
This is similar to this SO question, except that the solution posted there doesn't work (why is written below).
The C++ code:
ctxt->setContextProperty(QLatin1Literal("holiday"), m_model);
ctxt->setContextProperty(QLatin1Literal("bgcolor"), color);
view->setResizeMode(QQuickView::SizeRootObjectToView);
auto mainPath = QStandardPaths::locate(QStandardPaths::DataLocation,
QLatin1Literal("taskview.qml"));
view->setSource(QUrl::fromLocalFile(mainPath));
ctxt->setContextProperty(QLatin1Literal("viewer"), m_view);
m_view is a QListView subclass that emits the taskClicked(HolidayTask* task) signal (from the .h file):
Q_SIGNALS:
void taskClicked(HolidayTask* task);
color and m_model are registered in QML and are used elsewhere. The object from the signal is already registered in QML. view is my QQuickView.
First I tried the solution presented in the question above:
auto root = view->rootObject();
auto myElement = root->findChild<QObject*>(QLatin1Literal("testData");
connect(m_view, SIGNAL(taskClicked(HolidayTask* task), myElement,
SLOT(taskClicked(HolidayTask* task);
However, myElement is always null (and I get a runtime warning about a non existent slot).
If I try to set the view (the QListView) pointer as a context property of the QML view, it still doesn't work.
In all cases, I get also:
QML Connections: Cannot assign to non-existent property "onTaskClicked"
What could I possibly be doing wrong here?
EDIT to clarify some details: HolidayTask is a custom QObject subclass, and the signal taskClicked is defined in C++ (in a QListView subclass)
EDIT2: We're getting close, but no cigar:
auto root = quickView->rootObject();
auto myElement = root->findChild<QObject*>(QLatin1Literal("testData"));
connect(m_view, SIGNAL(taskClicked(HolidayTask*)),
myElement, SIGNAL(taskClicked(HolidayTask* task)));
and
Text {
id: testData
objectName: "testData"
signal taskClicked(HolidayTask task)
onTaskClicked: {
testData.text = task.name
console.log("CLICk!")
}
}
yields
QObject::connect: No such signal QQuickText_QML_0::taskClicked(HolidayTask* task) in /home/lb/Coding/cpp/holiday-planner/src/mainwindow.cpp:178
QObject::connect: (receiver name: 'testData')
More details: HolidayTask, my custom QObject subclass, is registered in the code as
qmlRegisterType<HolidayTask>("HolidayPlanner", 1, 0, "HolidayTask");
Minimal QML with the data:
import QtQuick 2.0
import QtQml 2.2
import HolidayPlanner 1.0
Rectangle {
id: container
objectName: "container"
color: bgcolor
Text {
id: testData
objectName: "testData"
signal taskClicked(HolidayTask task)
onTaskClicked: {
testData.text = task.name
console.log("CLICK")
}
}
}
EDIT3: The final, working code is (see answers on why)
connect(m_view, SIGNAL(taskClicked(HolidayPlanner::HolidayTask*)),
myElement, SIGNAL(taskClicked(HolidayPlanner::HolidayTask*)));
This worked only through the use of objects with full namespaces. Otherwise the signature will not match in QML.
However, myElement is always null (and I get a runtime warning about
a non existent slot).
You are trying to find the child based on id, whereas it is based on the objectName property. You would need to set the objectName property to the desired to actually find it.
Also, you do not seem to declare the signal in your QML Text item. I am not sure if it is a custom C++ item or a built-in one. You have not shared enough code unfortunately to understand that bit. Either way, declare your signal as per documentation.
Therefore, try this code out:
Text {
id: testData
objectName: "testData"
// ^^^^^^^^^^^^^^^^^^^
signal taskClicked (HolidayTask task)
// ^^^^^^^^^^^^^^^^^^^
onTaskClicked:{
testData.text = task.name
}
}
Once that is done, you are almost ready. You need to have your HolidayTask registered to QML for sure, and you also need to change the connect syntax in your main.cpp as follows:
connect(m_view, SIGNAL(taskClicked(HolidayTask* task), myElement, SIGNAL(taskClicked(HolidayTask* task)));
In short, you need to trigger your QML signal handler this way, and not via SLOT.
Also, note that your connect syntax is broken as it is missing the closing brackets at the end. That needs to be fixed.
I would even consider removing the pointer assignment and use value or reference based passing for this.
You can connect a signal from C++ to QML by:
view->rootContext()->setContextProperty("testData",this);
QObject::connect(this,SIGNAL(taskClicked(HolidayTask* task)),(QObject *)view->rootObject(),SLOT(onTaskClicked(HolidayTask* task)));
When your signal is named taskClicked, The slot in QML should be onTaskClicked.
Also in QML you should name the object testData by:
objectName: "testData"
QML Connections: Cannot assign to non-existent property "onTaskClicked"
The error tells you that your Text item do not have signal taskClicked or property onTaskClicked!
You need to declare a slot inside your text item. To do that, you simply declare a function:
Text {
id: testData
objectName: "testData" // as Laszlo said
function onTaskClicked( task ) {
testData.text = task.name;
}
}
But that also won't work because you create a SLOT( onTaskClicked(QVariant) ) instead of SLOT(taskClicked(HolidayTask*)). In order to exchange data with QML you need to change your signal to SIGNAL(taskClicked(QVariant)):
Q_SIGNALS:
void taskClicked(QVariant task);
And emit it with:
emit taskClicked( QVariant::fromValue( task ) );
Remember that in order to be able to use HolidayTask it must be a QObject that is registered with qmlRegisterType.
You can also simply call that qml function.
If you are not able to use QVariant you can declare a signal inside you Text object:
Text {
id: testData
objectName: "testData" // as Laszlo said
signal taskClicked ( HolidayTask task )
onTaskClicked: {
testData.text = task.name;
}
}
And then connect from a C++ SIGNAL to qml SIGNAL:
auto root = view->rootObject();
auto myElement = root->findChild<QObject*>(QLatin1Literal("testData");
connect(m_view, SIGNAL(taskClicked(HolidayTask*), myElement,
SIGNAL(taskClicked(HolidayTask*));