I've got an issue with getting the rows in the Recordset as it is really slow.
We've got an virtual ListCtrl where the data is retrieved and set in the "OnGetdispinfo" method.
This is pretty fast (~2 Seconds for 300k rows on localhost) however if the connection is slow the GUI becomes unrepsonsive and completly unusable until the job is finished.
So I've tried to do the Sql stuff in a different thread and updating the list once all data is fetched.
The issue with the unresponsive GUI is solved with that, but the time it takes to get all the data jumped from 2 seconds to several minutes.
Even if I dont do anything but loop through the rows (just calling MoveNext() in the loop until EOF is reached) it will still take over a minute to complete.
How do I resolve the issue with the freezing GUI without completly destroying the performance here?
I've included the relevant code below
m_pRecordset is a normal Recordset
Old:
void KundenListControlSQLCommand::OnGetdispinfo(NMHDR* pNMHDR, LRESULT* pResult)
{
if (m_pRecordset->IsBOF())
{
*pResult = 0;
return;
}
LV_DISPINFO* pDispInfo = (LV_DISPINFO*)pNMHDR;
LV_ITEM* pItem = &(pDispInfo)->item;
if (pItem->mask & LVIF_TEXT)
{
CString strData;
m_pRecordset->SetAbsolutePosition(pItem->iItem + 1);
if (getStatusRow() != pItem->iSubItem)
{
m_pRecordset->GetFieldValue(short(pItem->iSubItem), strData);
}
::lstrcpy(pItem->pszText, strData);
}
if (pItem->mask & LVIF_IMAGE)
{
int const nIndex = this->GetParent()->SendMessage(OT_VLC_ONGETIMAGEINDEX, pItem->iItem, 0);
if (0 != nIndex)
{
pItem->iImage = nIndex - 1;
}
}
*pResult = 0;
}
void KundenListControlSQLCommand::loadAndDisplayData()
{
ASSERT(!m_strSQLCommand.IsEmpty());
CWaitCursor wc;
try
{
if (!m_pDatabase->IsOpen())
{
CString strSQL = m_pDatabase->getDatabaseInfo().getConnectString();
m_pDatabase->OpenEx(strSQL);
}
// RecordCount ermitteln
m_nRecordCount = m_pRecordset->selectCount(_T("*"), m_strSQLCommand);
if (m_pRecordset->IsOpen())
m_pRecordset->Close();
m_pRecordset->Open(Recordset::snapshot, m_strSQLCommand + m_strSortOrder,
Recordset::executeDirect | Recordset::noDirtyFieldCheck |
Recordset::readOnly | Recordset::useBookmarks);
SetItemCountEx(m_nRecordCount);
}
catch (CDBException* e)
{
e->ReportError();
e->Delete();
}
}
New:
void KundenListControlSQLCommand::loadAndDisplayData()
{
ASSERT(!m_strSQLCommand.IsEmpty());
CWaitCursor wc;
try
{
if (!m_pDatabase->IsOpen())
{
CString strSQL = m_pDatabase->getDatabaseInfo().getConnectString();
m_pDatabase->OpenEx(strSQL);
}
// RecordCount ermitteln
m_nRecordCount = m_pRecordset->selectCount(_T("*"), m_strSQLCommand);
if (m_pRecordset->IsOpen())
m_pRecordset->Close();
m_pRecordset->Open(Recordset::dynaset, m_strSQLCommand + m_strSortOrder,
Recordset::executeDirect | Recordset::noDirtyFieldCheck |
Recordset::readOnly | Recordset::useBookmarks);
m_vResult.clear();
m_vResult.reserve(m_nRecordCount);
int nFieldCount = m_pRecordset->GetODBCFieldCount();
CString strData;
while (!m_pRecordset->IsEOF())
{
for (auto i = 0; i < nFieldCount; i++)
{
m_pRecordset->GetFieldValue(short(i), strData);
m_vResult.push_back(std::move(strData));
}
if (m_bAbort)
{
m_bAbort = false;
return;
}
m_pRecordset->MoveNext();
}
GetParent()->SendMessage(OT_VLC_ON_LIST_DONE, NULL, NULL);
}
catch (CDBException* e)
{
e->ReportError();
e->Delete();
}
}
void KundenListControlSQLCommand::OnGetdispinfo(NMHDR* pNMHDR, LRESULT* pResult)
{
if (m_pRecordset->IsBOF())
{
*pResult = 0;
return;
}
LV_DISPINFO* pDispInfo = (LV_DISPINFO*)pNMHDR;
LV_ITEM* pItem = &(pDispInfo)->item;
UINT nItem = (pItem->iItem * m_pRecordset->GetODBCFieldCount()) + pItem->iSubItem;
if (pItem->mask & LVIF_TEXT && m_vResult.size() >= nItem)
{
::lstrcpy(pItem->pszText, std::move(m_vResult.at(nItem)));
}
if (pItem->mask & LVIF_IMAGE)
{
int const nIndex = this->GetParent()->SendMessage(OT_VLC_ONGETIMAGEINDEX, pItem->iItem, 0);
if (0 != nIndex)
{
pItem->iImage = nIndex - 1;
}
}
*pResult = 0;
}``
As I can see in your code, you read the data and place them into the vector. In such a setting, I think you don't really need a dynaset recordset, which according to the documentation is "A recordset with bi-directional scrolling". It fetches data row-by-row, which may be what makes the process slow. Also, "changes made by other users to the data values are visible following a fetch operation", but I think this is not of critical importance in this case. It would be mostly useful for displaying more "live" data, that are updated often.
Instead, a snapshot, or even forwardOnly recordset would suffice and would be faster. You can also experiment with the CRecordset::useMultiRowFetch option. The documentation says it's faster. It requires some changes to your code (moving next etc). Take a look here: Recordset: Fetching Records in Bulk (ODBC).
An alternative, radically different implementation would be to use bookmarks instead. Loading would be a lot faster, but scrolling somewhat sluggish, as you will have to fetch data in the OnGetdispinfo() function.
Finally a tip, if you are using the MS-SQL server, check the native driver, if you haven't already, many on the i-net claim that it's considerably faster.
I don't know much about ODBC, but suspect that there are better way to get bulk data.
Regardless, you do a lot of unnecessary copying of your vectors. Two easy fixes:
Right after m_vResult.clear();, resize your m_vResult to the number of records.
Instead of m_vResult.push_back(vResult); do m_vResult.push_back(std::move(vResult));, as you don't need your vResult after that.
Another solution is to do a cache list, handling LVN_ODCACHEHINT notification (this example is for CListView but you can adapt it on your CListCtrl:
// header.h
class CYourListView : public CListView
{
// ...
afx_msg void OnLvnOdcachehint(NMHDR* pNMHDR, LRESULT *pResult);
};
and implementation:
// YourListView.cpp
// ...
ON_NOTIFY_REFLECT(LVN_ODCACHEHINT, &CYourListView::OnLvnOdcachehint)
END_MESSAGE_MAP()
void CYourListView::OnLvnOdcachehint(NMHDR* pNMHDR, LRESULT* pResult)
{
LPNMLVCACHEHINT pCacheHint = reinterpret_cast<LPNMLVCACHEHINT>(pNMHDR);
const DWORD dwTo = pCacheHint->iTo;
const DWORD dwFetched = m_vResult.size();
if (dwTo >= dwFetched) // new rows must be fetched
{
const DWORD dwColCount = m_pRecordset->GetColumnCount();
m_vResult.resize(dwTo + 1);
for (DWORD dwRow = dwFetched; dwRow <= dwTo; ++dwRow)
{
CDBRecord* pRecord = new CDBRecord;
pRecord->SetSize(dwColCount);
for (DWORD dwCol = 1; dwCol <= dwColCount; dwCol++)
{
CDBValue* pDBValue = new CDBValue(m_pRecordset, dwCol);
pRecord->SetAt(dwCol - 1, pDBValue);
}
m_vResult.emplace(m_vResult.begin() + dwRow, pRecord);
m_pRecordset->MoveNext();
}
}
*pResult = 0;
}
might be need to adjust some variables / values with your certain situation.
Related
I'm working on a wrapper for MariaDB Connector C. There is a typical situation when a developer doesn't know a length of a data stored in a field. As I figured out, one of the ways to obtain a real length of the field is to pass a buffer of lengths to mysql_stmt_bind_result and then to fetch each column by calling mysql_stmt_fetch_column. But I can't understand how the function mysql_stmt_fetch_column works because I'm getting a memory corruption and app abortion.
Here is how I'm trying to reach my goal
// preparations here
...
if (!mysql_stmt_execute(stmt))
{
int columnNum = mysql_stmt_field_count(stmt);
if (columnNum > 0)
{
MYSQL_RES* metadata = mysql_stmt_result_metadata(stmt);
MYSQL_FIELD* fields = mysql_fetch_fields(metadata);
MYSQL_BIND* result = new MYSQL_BIND[columnNum];
std::memset(result, 0, sizeof (MYSQL_BIND) * columnNum);
std::vector<unsigned long> lengths;
lengths.resize(columnNum);
for (int i = 0; i < columnNum; ++i)
result[i].length = &lengths[i];
if (!mysql_stmt_bind_result(stmt, result))
{
while (true)
{
int status = mysql_stmt_fetch(stmt);
if (status == 1)
{
m_lastError = mysql_stmt_error(stmt);
isOK = false;
break;
}
else if (status == MYSQL_NO_DATA)
{
isOK = true;
break;
}
for (int i = 0; i < columnNum; ++i)
{
my_bool isNull = true;
if (lengths.at(i) > 0)
{
result[i].buffer_type = fields[i].type;
result[i].is_null = &isNull;
result[i].buffer = malloc(lengths.at(i));
result[i].buffer_length = lengths.at(i);
mysql_stmt_fetch_column(stmt, result, i, 0);
if (!isNull)
{
// here I'm trying to read a result and I'm getting a valid result only from the first column
}
}
}
}
}
}
If I put an array to the mysql_stmt_fetch_column then I'm fetching the only first field valid, all other fields are garbage. If I put a single MYSQL_BIND structure to this function, then I'm getting an abortion of the app on approximately 74th field (funny thing that it's always this field). If I use another array of MYSQL_BIND then the situation is the same as the first case.
Please help me to understand how to use it correctly! Thanks
Minimal reproducible example
I am working a on database than runs on top on RocksDB. I have a find function that takes a query in parameter, iterates over all documents in the database, and returns the documents that match the query. I want to parallelize this function so the work is spread on multiple threads.
To achieve that, I tried to use ThreadPool: I moved the code of the loop in a lambda, and added a task to the thread pool for each document. After the loop, each result is processed by the main thread.
Current version (single thread):
void
EmbeDB::find(const bson_t& query,
DocumentPtrCallback callback,
int32_t limit,
const bson_t* projection)
{
int32_t count = 0;
bson_error_t error;
uint32_t num_query_keys = bson_count_keys(&query);
mongoc_matcher_t* matcher = num_query_keys != 0
? mongoc_matcher_new(&query, &error)
: nullptr;
if (num_query_keys != 0 && matcher == nullptr)
{
callback(&error, nullptr);
return;
}
bson_t document;
rocksdb::Iterator* it = _db->NewIterator(rocksdb::ReadOptions());
for (it->SeekToFirst(); it->Valid(); it->Next())
{
const char* bson_data = (const char*)it->value().data();
int bson_length = it->value().size();
std::vector<char> decrypted_data;
if (encryptionEnabled())
{
decrypted_data.resize(bson_length);
bson_length = decrypt_data(bson_data, bson_length, decrypted_data.data(), _encryption_method, _encryption_key, _encryption_iv);
bson_data = decrypted_data.data();
}
bson_init_static(&document, (const uint8_t*)bson_data, bson_length);
if (num_query_keys == 0 || mongoc_matcher_match(matcher, &document))
{
++count;
if (projection != nullptr)
{
bson_error_t error;
bson_t projected;
bson_init(&projected);
mongoc_matcher_projection_execute_noop(
&document,
projection,
&projected,
&error,
NULL
);
callback(nullptr, &projected);
}
else
{
callback(nullptr, &document);
}
if (limit >= 0 && count >= limit)
{
break;
}
}
}
delete it;
if (matcher)
{
mongoc_matcher_destroy(matcher);
}
}
New version (multi-thread):
void
EmbeDB::find(const bson_t& query,
DocumentPtrCallback callback,
int32_t limit,
const bson_t* projection)
{
int32_t count = 0;
bool limit_reached = limit == 0;
bson_error_t error;
uint32_t num_query_keys = bson_count_keys(&query);
mongoc_matcher_t* matcher = num_query_keys != 0
? mongoc_matcher_new(&query, &error)
: nullptr;
if (num_query_keys != 0 && matcher == nullptr)
{
callback(&error, nullptr);
return;
}
auto process_document = [this, projection, num_query_keys, matcher](const char* bson_data, int bson_length) -> bson_t*
{
std::vector<char> decrypted_data;
if (encryptionEnabled())
{
decrypted_data.resize(bson_length);
bson_length = decrypt_data(bson_data, bson_length, decrypted_data.data(), _encryption_method, _encryption_key, _encryption_iv);
bson_data = decrypted_data.data();
}
bson_t* document = new bson_t();
bson_init_static(document, (const uint8_t*)bson_data, bson_length);
if (num_query_keys == 0 || mongoc_matcher_match(matcher, document))
{
if (projection != nullptr)
{
bson_error_t error;
bson_t* projected = new bson_t();
bson_init(projected);
mongoc_matcher_projection_execute_noop(
document,
projection,
projected,
&error,
NULL
);
delete document;
return projected;
}
else
{
return document;
}
}
else
{
delete document;
return nullptr;
}
};
const int WORKER_COUNT = std::max(1u, std::thread::hardware_concurrency());
ThreadPool pool(WORKER_COUNT);
std::vector<std::future<bson_t*>> futures;
bson_t document;
rocksdb::Iterator* db_it = _db->NewIterator(rocksdb::ReadOptions());
for (db_it->SeekToFirst(); db_it->Valid(); db_it->Next())
{
const char* bson_data = (const char*)db_it->value().data();
int bson_length = db_it->value().size();
futures.push_back(pool.enqueue(process_document, bson_data, bson_length));
}
delete db_it;
for (auto it = futures.begin(); it != futures.end(); ++it)
{
bson_t* result = it->get();
if (result)
{
count += 1;
if (limit < 0 || count < limit)
{
callback(nullptr, result);
}
delete result;
}
}
if (matcher)
{
mongoc_matcher_destroy(matcher);
}
}
With simple documents and query, the single-thread version processes 1 million documents in 0.5 second on my machine.
With the same documents and query, the multi-thread version processes 1 million documents in 3.3 seconds.
Surprisingly, the multi-thread version is way slower. Moreover, I measured the execution time and 75% of the time is spent in the for loop. So basically the line futures.push_back(pool.enqueue(process_document, bson_data, bson_length)); takes 75% of the time.
I did the following:
I checked the value of WORKER_COUNT, it is 6 on my machine.
I tried to add futures.reserve(1000000), thinking that maybe the vector re-allocation was at fault, but it didn't change anything.
I tried to remove the dynamic memory allocations (bson_t* document = new bson_t();), it didn't change the result significantly.
So my question is: is there something that I did wrong for the multi-thread version to be that slower than the single-thread version?
My current understanding is that the synchronization operations of the thread pool (when tasks are enqueued and dequeued) are simply consuming the majority of the time, and the solution would be to change the data-structure. Thoughts?
Parallelization has overhead.
It takes around 500 nanoseconds to process each document in the single-threaded version. There's a lot of bookkeeping that has to be done to delegate work to a thread-pool (both to delegate the work, and to synchronize it afterwards), and all that bookkeeping could very well require more than 500 nanoseconds per job.
Assuming your code is correct, then the bookkeeping takes around 2800 nanoseconds per job. To get a significant speedup from parallelization, you're going to want to break the work into bigger chunks.
I recommend trying to process documents in batches of 1000 at a time. Each future, instead of corresponding to just 1 document, will correspond to 1000 documents.
Other optimizations
If possible, avoid unnecessary copying. If something gets copied a bunch, see if you can capture it by reference instead of by value.
I'm writing a little Console-Game-Engine and for better performance I wanted 2 threads (or more but 2 for this task) using two buffers. One thread is drawing the next frame in the first buffer while the other thread is reading the current frame from the second buffer. Then the buffers get swapped.
Of cause I can only swap them if both threads finished their task and the drawing/writing thread happened to be the one waiting. But the time it is waiting systematicly switches more or less between two values, here a few of the messurements I made (in microseconds):
0, 36968, 0, 36260, 0, 35762, 0, 38069, 0, 36584, 0, 36503
It's pretty obvious that this is not a coincidence but I wasn't able to figure out what the problem was as this is the first time I'm using threads.
Here the code, ask for more if you need it, I think it's too much to post it all:
header-file (Manager currently only adds a pointer to my WinAppBase-class):
class SwapChain : Manager
{
WORD *pScreenBuffer1, *pScreenBuffer2, *pWritePtr, *pReadPtr, *pTemp;
bool isRunning, writingFinished, readingFinished, initialized;
std::mutex lockWriting, lockReading;
std::condition_variable cvWriting, cvReading;
DWORD charsWritten;
COORD startPosition;
int screenBufferWidth;
// THREADS (USES NORMAL THREAD AS SECOND THREAD)
void ReadingThread();
// THIS FUNCTION IS ONLY FOR INTERN USE
void SwapBuffers();
public:
// USE THESE TO CONTROL WHEN THE BUFFERS GET SWAPPED
void BeginDraw();
void EndDraw();
// PUT PIXEL | INLINED FOR BETTER PERFORMANCE
inline void PutPixel(short xPos, short yPos, WORD color)
{
this->pWritePtr[(xPos * 2) + yPos * screenBufferWidth] = color;
this->pWritePtr[(xPos * 2) + yPos * screenBufferWidth + 1] = color;
}
// GENERAL CONTROL OVER SWAP CHAIN
void Initialize();
void Run();
void Stop();
// CONSTRUCTORS
SwapChain(WinAppBase * pAppBase);
virtual ~SwapChain();
};
Cpp-file
SwapChain::SwapChain(WinAppBase * pAppBase)
:
Manager(pAppBase)
{
this->isRunning = false;
this->initialized = false;
this->pReadPtr = NULL;
this->pScreenBuffer1 = NULL;
this->pScreenBuffer2 = NULL;
this->pWritePtr = NULL;
this->pTemp = NULL;
this->charsWritten = 0;
this->startPosition = { 0, 0 };
this->readingFinished = 0;
this->writingFinished = 0;
this->screenBufferWidth = this->pAppBase->screenBufferInfo.dwSize.X;
}
SwapChain::~SwapChain()
{
this->Stop();
if (_CrtIsValidHeapPointer(pReadPtr))
delete[] pReadPtr;
if (_CrtIsValidHeapPointer(pScreenBuffer1))
delete[] pScreenBuffer1;
if (_CrtIsValidHeapPointer(pScreenBuffer2))
delete[] pScreenBuffer2;
if (_CrtIsValidHeapPointer(pWritePtr))
delete[] pWritePtr;
}
void SwapChain::ReadingThread()
{
while (this->isRunning)
{
this->readingFinished = 0;
WriteConsoleOutputAttribute(
this->pAppBase->consoleCursor,
this->pReadPtr,
this->pAppBase->screenBufferSize,
this->startPosition,
&this->charsWritten
);
memset(this->pReadPtr, 0, this->pAppBase->screenBufferSize);
this->readingFinished = true;
this->cvWriting.notify_all();
if (!this->writingFinished)
{
std::unique_lock<std::mutex> lock(this->lockReading);
this->cvReading.wait(lock);
}
}
}
void SwapChain::SwapBuffers()
{
this->pTemp = this->pReadPtr;
this->pReadPtr = this->pWritePtr;
this->pWritePtr = this->pTemp;
this->pTemp = NULL;
}
void SwapChain::BeginDraw()
{
this->writingFinished = false;
}
void SwapChain::EndDraw()
{
TimePoint tpx1, tpx2;
tpx1 = Clock::now();
if (!this->readingFinished)
{
std::unique_lock<std::mutex> lock2(this->lockWriting);
this->cvWriting.wait(lock2);
}
tpx2 = Clock::now();
POST_DEBUG_MESSAGE(std::chrono::duration_cast<std::chrono::microseconds>(tpx2 - tpx1).count(), "EndDraw wating time");
SwapBuffers();
this->writingFinished = true;
this->cvReading.notify_all();
}
void SwapChain::Initialize()
{
if (this->initialized)
{
POST_DEBUG_MESSAGE(Result::CUSTOM, "multiple initialization");
return;
}
this->pScreenBuffer1 = (WORD *)malloc(sizeof(WORD) * this->pAppBase->screenBufferSize);
this->pScreenBuffer2 = (WORD *)malloc(sizeof(WORD) * this->pAppBase->screenBufferSize);
for (int i = 0; i < this->pAppBase->screenBufferSize; i++)
{
this->pScreenBuffer1[i] = 0x0000;
}
for (int i = 0; i < this->pAppBase->screenBufferSize; i++)
{
this->pScreenBuffer2[i] = 0x0000;
}
this->pWritePtr = pScreenBuffer1;
this->pReadPtr = pScreenBuffer2;
this->initialized = true;
}
void SwapChain::Run()
{
this->isRunning = true;
std::thread t1(&SwapChain::ReadingThread, this);
t1.detach();
}
void SwapChain::Stop()
{
this->isRunning = false;
}
This is where I run the SwapChain-class from:
void Application::Run()
{
this->engine.graphicsmanager.swapChain.Initialize();
Sprite<16, 16> sprite(&this->engine);
sprite.LoadSprite("engine/resources/TestData.xml", "root.test.sprites.baum");
this->engine.graphicsmanager.swapChain.Run();
int a, b, c;
for (int i = 0; i < 60; i++)
{
this->engine.graphicsmanager.swapChain.BeginDraw();
for (c = 0; c < 20; c++)
{
for (a = 0; a < 19; a++)
{
for (b = 0; b < 10; b++)
{
sprite.Print(a * 16, b * 16);
}
}
}
this->engine.graphicsmanager.swapChain.EndDraw();
}
this->engine.graphicsmanager.swapChain.Stop();
_getch();
}
The for-loops above simply draw the sprite 20 times from the top-left corner to the bottom-right corner of the console - the buffers don't get swapped during that, and that again for a total of 60 times (so the buffers get swapped 60 times).
sprite.Print uses the PutPixel function of SwapChain.
Here the WinAppBase (which consits more or less of global-like variables)
class WinAppBase
{
public:
// SCREENBUFFER
CONSOLE_SCREEN_BUFFER_INFO screenBufferInfo;
long screenBufferSize;
// CONSOLE
DWORD consoleMode;
HWND consoleWindow;
HANDLE consoleCursor;
HANDLE consoleInputHandle;
HANDLE consoleHandle;
CONSOLE_CURSOR_INFO consoleCursorInfo;
RECT consoleRect;
COORD consoleSize;
// FONT
CONSOLE_FONT_INFOEX fontInfo;
// MEMORY
char * pUserAccessDataPath;
public:
void reload();
WinAppBase();
virtual ~WinAppBase();
};
There are no errors, simply this alternating waitng time.
Maybe you'd like to start by looking if I did the synchronisation of the threads correctly? I'm not exactly sure how to use a mutex or condition-variables so it might comes from that.
Apart from that it is working fine, the sprites are shown as they should.
The clock you are using may have limited resolution. Here is a random example of a clock provided by Microsoft with 15 ms (15000 microsecond) resolution: Why are .NET timers limited to 15 ms resolution?
If one thread is often waiting for the other, it is entirely possible (assuming the above clock resolution) that it sometimes waits two clockticks and sometimes none. Maybe your clock only has 30 ms resolution. We really can't tell from the code. Do you get more precise measurements elsewhere with this clock?
There are also other systems in play such as the OS scheduler or whatever controls your std::threads. That one is (hopefully) much more granular, but how all these interactions play out doesn't have to be obvious or intuitive.
I'm currently using a derived class of CListCtrl to build my application upon. I'm trying to add an item to the list, and immediately assign it to a group:
// int row, int grp_id, CString header; - all initialized previously
int ind = m_list.InsertItem(row, header);
VERIFY(m_list.SetRowGroupId(row, grp_id));
Here's an implementation of SetRowGroupID():
BOOL CGridListCtrlGroups::SetRowGroupId(int nRow, int nGroupId)
{
//OBS! Rows not assigned to a group will not show in group-view
LVITEM lvItem = { 0 };
lvItem.mask = LVIF_GROUPID;
lvItem.iItem = nRow;
lvItem.iSubItem = 0;
lvItem.iGroupId = nGroupId;
return SetItem(&lvItem);
}
Nothing too fancy here.
However, the code is asserting false for VERIFY().
While I was searching through MSDN documents to find out why, it seems that the flag I want to use as a mask isn't available for this purpose (refer to Remarks).
Am I not able to change the group ID this way? For the record, I've also tried to use MoveItemToGroup(), which gave me the same result (that is, the item doesn't show up while group view is enabled).
I cannot say why updating a member's group ID does not work directly.
However, setting .iGroupId = I_GROUPIDCALLBACK triggers the message LVN_GETDISPINFO and in its handler you can then assign the group ID.
Outline:
BEGIN_MESSAGE_MAP(CGridListCtrlGroups, CListCtrl)
ON_NOTIFY_REFLECT(LVN_GETDISPINFO, OnGetDispInfo)
END_MESSAGE_MAP()
void CGridListCtrlGroups::CGridListCtrlGroups(NMHDR* pNMHDR, LRESULT* pResult)
{
NMLVDISPINFO* dispInfo = LPNMLVDISPINFOW(pNMHDR);
LVITEM* item = &dispInfo->item;
if (item->mask & LVIF_GROUPID) {
// assign group id
item->iGroupId = ...;
}
*pResult = 0;
}
BOOL CGridListCtrlGroups::RequeryRowGroupId(int nRow)
{
LVITEM lvi = {
.mask = LVIF_GROUPID,
.iItem = nRow,
.iGroupId = I_GROUPIDCALLBACK
};
return SetItem(&lvi);
}
I have tried all the normal methods of faking keyboard actions (SendInput/SendKeys/etc) but none of them seemed to work for games that used DirectInput. After a lot of reading and searching I stumbled across Interception, which is a C++ Library that allows you to hook into your devices.
It has been a very long time since I worked with C++ (Nothing existed for C#) so I am having some trouble with this. I have pasted in the sample code below.
Does it look like there would be anyway to initiate key actions from the code using this? The samples all just hook into the devices and rewrite actions (x key prints y, inverts mouse axis, etc).
enum ScanCode
{
SCANCODE_X = 0x2D,
SCANCODE_Y = 0x15,
SCANCODE_ESC = 0x01
};
int main()
{
InterceptionContext context;
InterceptionDevice device;
InterceptionKeyStroke stroke;
raise_process_priority();
context = interception_create_context();
interception_set_filter(context, interception_is_keyboard, INTERCEPTION_FILTER_KEY_DOWN | INTERCEPTION_FILTER_KEY_UP);
/*
for (int i = 0; i < 10; i++)
{
Sleep(1000);
stroke.code = SCANCODE_Y;
interception_send(context, device, (const InterceptionStroke *)&stroke, 1);
}
*/
while(interception_receive(context, device = interception_wait(context), (InterceptionStroke *)&stroke, 1) > 0)
{
if(stroke.code == SCANCODE_X) stroke.code = SCANCODE_Y;
interception_send(context, device, (const InterceptionStroke *)&stroke, 1);
if(stroke.code == SCANCODE_ESC) break;
}
The code I commented out was something I tried that didn't work.
You need to tweak key states for UP and DOWN states to get key presses. Pay attention at the while loop that the variable device is returned by interception_wait, your commented out code would send events to what?? device is not initialized! Forget your code and try some more basic. Look at the line inside the loop with the interception_send call, make more two calls after it, but don't forget to change stroke.state before each call using INTERCEPTION_KEY_DOWN and INTERCEPTION_KEY_UP so that you fake down and up events. You'll get extra keys at each keyboard event.
Also, you may try use INTERCEPTION_FILTER_KEY_ALL instead of INTERCEPTION_FILTER_KEY_DOWN | INTERCEPTION_FILTER_KEY_UP. The arrow keys may be special ones as mentioned at the website.
void ThreadMethod()
{
while (true)
{
if (turn)
{
for (int i = 0; i < 10; i++)
{
Sleep(1000);
InterceptionKeyStroke stroke;
stroke.code = SCANCODE_Y;
stroke.state = 0;
interception_send(context, device, (const InterceptionStroke *)&stroke, 1);
Sleep(1);
stroke.state = 1;
interception_send(context, device, (const InterceptionStroke *)&stroke, 1);
turn = false;
}
}
else Sleep(1);
}
}
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)ThreadMethod, NULL, NULL, NULL);
while (interception_receive(context, device = interception_wait(context), (InterceptionStroke*)&stroke, 1) > 0)
{
if (stroke.code == SCANCODE_F5) turn = true;
interception_send(context, device, (InterceptionStroke*)&stroke, 1);
if (stroke.code == SCANCODE_ESC) break;
}