sudoku me_Sudoku,一个完整的MFC应用程序。 第8部分

sudoku me

介绍: (Introduction:)

数据库存储,光盘上的exe实际在哪里? 玩随机选择的游戏(如何生成随机数)。 使用try..catch捕获错误可以帮助代码运行,即使出现问题。

Continuing from the seventh article about sudoku.  

续上关于数独的第七篇文章。

Random number generators.  There is a built in random number function in windows.  For playing our games of sudoku having a good random number generating algorithm isn’t necessary.  For other work it may well be critical, put another way having a poor algorithm could result in you generating rubbish from your application – because the data it is based on was not truly random.  Here we are going to provide a basic class for random number generation using a technique (which is supposed to be good) from literature, specifically about mathematical techniques in programming.  Don’t take this as any implication that the inbuilt routines are poor quality or unreliable.  The source of the algorithm here is referenced in the code.

随机数生成器。 Windows中有一个内置的随机数功能。 对于玩我们的数独游戏,没有一个好的随机数生成算法是没有必要的。 对于其他工作,这可能很关键,换种说法是,算法较差会导致您从应用程序中产生垃圾-因为它所基于的数据并不是真正的随机数据。 在这里,我们将提供一个使用文献中的技术(应该是好的)来生成随机数的基本类,特别是关于编程中的数学技术。 不要以为这意味着内置例程质量差或不可靠。 此处引用了算法的源代码。

I have created a new database with Microsoft Access, in Access 2000 format, and called it Sudoku.mdb. The database must be located in the same directory as the exe file.  The database contains one table (Games), this contains two fields ID (auto number, primary key) and GameDetail (Text, 82 char, required=true, Allow zero length=false, indexed=yes – no duplicates).

我已经使用Access 2000格式使用Microsoft Access创建了一个新数据库,并将其命名为Sudoku.mdb。 数据库必须与exe文件位于同一目录中 。 该数据库包含一个表(游戏),其中包含两个字段ID(自动编号,主键)和GameDetail(文本,82个字符,必填= true,允许零长度= false,索引= yes –不重复)。

Open the project in visual studio.  Add a new C++ class (not MFC) to the project via the solution explorer and call it CRandomNumber.  The following are the .h and .cpp file contents respectively.

在Visual Studio中打开项目。 通过解决方案资源管理器将新的C ++类(不是MFC)添加到项目中,并将其命名为CRandomNumber。 以下分别是.h和.cpp文件的内容。

#pragma once

class CRandomNumber
{
public:
    CRandomNumber(long lSeed = 0);
    ~CRandomNumber(void) {};

    long GetRandom(long lLower = 0, long lUpper = 0x7FFFFFFE);
private:
    long m_lSeed;
    float rand(long *idum);

    //Following are static members of the func in the original
    //here made to class members to preserve values between calls
    //but allows multiple instances of the class to be independant of one another
    //(should that be required)
    long iy;
    long iv[32];    //array size MUST be the same as value of NTAB
};

and

#include "StdAfx.h"
#include "RandomNumber.h"


#define IA (16807)
#define IM (2147483647)
#define AM ((float)1.0/(float)IM)
#define IQ (127773)
#define IR (2836)
#define NTAB (32)
#define NDIV (1+(IM-1)/NTAB)
#define EPS ((float)1.2e-7)
#define RNMX ((float)(1.0-EPS))


CRandomNumber::CRandomNumber(long lSeed)
: m_lSeed(lSeed > 0 ? -lSeed : lSeed)
, iy(0)
{
    //Check that the 'iv' array is the same size as NTAB - it should be so
    ASSERT(sizeof(iv) == NTAB * sizeof(long));

    if(m_lSeed == 0)
    {
        //generate a seed, use the day of year and time of day as a base value
        COleDateTime dte = COleDateTime::GetCurrentTime();
        m_lSeed = -(dte.GetDayOfYear() * 24 * 60 * 60 + dte.GetHour() * 60 * 60 + dte.GetMinute() * 60 + dte.GetSecond());
    }
    rand(&m_lSeed);
}

//Assume no overflows of the entered values, and that lLower is in fact less than lUpper
long CRandomNumber::GetRandom(long lLower, long lUpper)
{
    if(lLower == lUpper)
        return lLower;

    float fRes = rand(&m_lSeed);

    //lUpper would never be returned if this value was not incremented
    lUpper++;

    return (long)((float)(lUpper - lLower) * fRes) + lLower;
}


float CRandomNumber::rand(long *idum)
{
    int j;
    long k;
    float temp;

    if(idum <= 0 || !iy)    //initialise
    {
        if(-(*idum) < 1)    //idum should not be zero, if it is then set to 1 as an initial value
            *idum = 1;
        else
            *idum = -(*idum);    //make positive

        for(j = NTAB+7; j >= 0; j--)
        {
            k = (*idum)/IQ;
            *idum = IA * (*idum - k*IQ) - IR*k;
            if(*idum < 0)
                *idum += IM;
            if(j < NTAB)
                iv[j] = *idum;
        }
        iy = iv[0];
    }

    //Starts here when not initialising
    k = (*idum) / IQ;

    //computes idum=(IA*idum) % IM without overflows by Schrage's method
    *idum = IA * (*idum - k*IQ) - IR*k;
    if(*idum < 0)
        *idum += IM;

    j = iy / NDIV;    //range 0..NTAB-1

    //Get previous value then replace the array with the new value
    iy = iv[j];
    iv[j] = *idum;

    temp = AM * iy;
    
    //In case temp is over the max value set it to the max value
    if(temp > RNMX)
        temp = RNMX;

    //the result is returned
    return temp;
}


/*
The following is the source code from the literature:
Numerical recipies in C, second edition
W.H.Press, S.A.Teukolsky, W.T.Vetterling, B.P.Flannery

chapter 7, ran1 method for random number generation


#define IA 16807
#define IM 2147483647
#define AM ((float)1.0/IM)
#define IQ 127773
#define IR 2836
#define NTAB 32
#define NDIV (1+(IM-1)/NTAB)
#define EPS (float)1.2e-7
#define RNMX (float)(1.0-EPS)

float RAND(long *idum)
{
    int j;
    long k;
    static long iy=0;
    static long iv[NTAB];
    float temp;

    if(idum <= 0 || !iy)    //initialise
    {
        if(-(*idum) < 1)    //idum should not be zero, if it is then set to 1 as an initial value
            *idum = 1;
        else
            *idum = -(*idum);    //make positive

        for(j = NTAB+7; j >= 0; j--)
        {
            k = (*idum)/IQ;
            *idum = IA * (*idum - k*IQ) - IR*k;
            if(*idum < 0)
                *idum += IM;
            if(j < NTAB)
                iv[j] = *idum;
        }
        iy = iv[0];
    }

    //Starts here when not initialising
    k = (*idum) / IQ;

    //computes idum=(IA*idum) % IM without overflows by Schrage's method
    *idum = IA * (*idum - k*IQ) - IR*k;
    if(*idum < 0)
        *idum += IM;

    j = iy / NDIV;    //range 0..NTAB-1

    //Get previous value then replace the array with the new value
    iy = iv[j];
    iv[j] = *idum;

    temp = AM * iy;
    
    //In case temp is over the max value set it to the max value
    if(temp > RNMX)
        temp = RNMX;

    //the result is returned
    return temp;
}
*/

We now need to modify the document to support an Access database.  To the SudokuDoc.h file add the following at the top after the #pragma once

现在,我们需要修改文档以支持Access数据库。 在SudokuDoc.h文件中,在#pragma一次之后的顶部添加以下内容

#include "afxdao.h"
#pragma warning(disable : 4995)

And in the class we need a new variable

在课堂上,我们需要一个新变量

private:
    CDaoDatabase* m_pDB;

Go to the constructor and modify it to be the following:

转到构造函数并将其修改为以下内容:

CSudokuDoc::CSudokuDoc()
{
    //Create the pointer to the database
    m_pDB = new CDaoDatabase();

    //Get the path to the database and open it
    //Note, a common fault is to rely on finding the current directory and ASSUMING that is where the 
    //application.exe is located.  Unfortunately as part of a startup of an exe one can specify the directory 
    //to start in so that assumption may work for you in testing, but a user can easily make it fail.
    //You get the fallout (--> the app doesn't work is the feedback to your boss? )

    char szPath[MAX_PATH + 1];    //plus space for 0 string terminator
    GetModuleFileName(NULL, szPath, MAX_PATH);    //NULL = use this module (the app)

    //Now remove the name of the exe from the returned string - find the final '\' char in the string
    char* pch = strrchr(szPath, '\\');
    *pch = 0;    //insert a 0 to act as a string terminator
    strcat_s(szPath, MAX_PATH, "\\Sudoku.mdb");  //append the name of the mdb

    //open the database
    m_pDB->Open(szPath);
}

Read the comments in the constructor code – that is how (and why) one finds where the exe is on disc when it is running.  What is the following line of code?

阅读构造函数代码中的注释-这就是( 为什么 )在运行时发现exe在光盘上的位置。 下面的代码行是什么?

#pragma warning(disable : 4995)

MFC provides a suite of classes to work specifically with Access databases.  Microsoft has also decided they will at some time no longer be supported (however they are still there in VS 2010 so they should be available for a number of years).  Note however the DAO classes are NOT supported for 64 bit development in Visual Studio 2010 (and possibly earlier versions of Visual Studio).

MFC提供了一组类,专门用于Access数据库。 微软还决定不再支持它们(但是在VS 2010中它们仍然存在,因此它们应该可以使用很多年)。 但是请注意,Visual Studio 2010(以及可能的早期版本的Visual Studio)中的64位开发不支持DAO类。

At compile time the compiler will give a warning that the DAO classes will be dropped at some time – this line of code suppresses that warning, specifically it hides the warning with the ID 4995.  (Warnings are that – something might be wrong with the code, but then again it doesn’t mean it will fail.  Take note of compiler warnings, check them and take appropriate action.)

在编译时,编译器将发出警告,提示DAO类将在某些时候被丢弃–此行代码禁止了该警告,特别是它隐藏了ID 4995的警告。(警告是–代码可能有问题,但这并不意味着它会失败。请注意编译器警告,进行检查并采取适当的措施。)

Now we need some cleanup code in the destructor

现在我们需要析构函数中的一些清理代码

CSudokuDoc::~CSudokuDoc()
{
    if(m_pDB != NULL)
    {
        m_pDB->Close();
        delete m_pDB;
        m_pDB = NULL;
    }
}

We assigned memory with the 'new' keyword, now we release it otherwise we have a memory leak.  When the app is closed one would see warnings about memory leaks in the debug window.  (Rant time:  I personally hate the garbage collection mechanism in .net – during testing a memory leak, should one ever appear, tells me that there is a logic error (and that happens even with the best of us no matter how careful one is).  The gc mechanism stops that useful information, it even promotes sloppy coding by not having to think about cleaning up memory you assign.  We should now have a database connection – simple wasn’t it – so lets use it.  ps. The supplied database has a few games already stored in it.

我们使用'new'关键字分配了内存,现在我们释放它,否则会发生内存泄漏。 关闭应用程序后,将在调试窗口中看到有关内存泄漏的警告。 (

Now open the resource editor.  From the menu resources select the ‘Play Random’ entry, right click with the mouse and adds a new event handler.  Select COMMAND as the type, enter OnFilePlayrandom as the name and from the class list select the CSudokuDoc, now click the Add and Edit button.  Modify this new function to be as follows:

现在打开资源编辑器。 从菜单资源中选择“随机播放”条目,用鼠标右键单击并添加一个新的事件处理程序。 选择COMMAND作为类型,输入OnFilePlayrandom作为名称,然后从类列表中选择CSudokuDoc,现在单击Add and Edit按钮。 修改此新功能,如下所示:

    CRandomNumber rn;

    CDaoRecordset RS(m_pDB);
    RS.Open(AFX_DAO_USE_DEFAULT_TYPE, "SELECT ID, GameDetail FROM Games");
    RS.MoveLast();
    long lCount = RS.GetRecordCount();
    RS.MoveFirst();
    
    //move to random record (eg. 5 records then VALID moves are 0, 1, 2, 3, 4 -- so we need lCount-1)
    RS.Move(rn.GetRandom(0, lCount - 1));

    COleVariant var = RS.GetFieldValue("GameDetail");
    //It is a string in the table, now convert it to a string we can use
    CString szGame(var.pbVal);

    //Now for the info to display in the status bar
    var = RS.GetFieldValue("ID");
    m_szInfo.Format("ID: %ld", var.lVal);
    
    RS.Close();

    strcpy_s(m_arGame, sizeof(m_arGame), szGame);
    
    UpdateAllViews(NULL, eLoadGame, NULL);

And don’t forget to include the header file for the random number class

并且不要忘记为随机数类包括头文件

#include "SudokuDoc.h"
#include "RandomNumber.h"

That should be pretty clear to understand.  Note I can base a recordset directly on an SQL command, it doesn’t have to be a table or a query stored in the database.  You should be able to compile and test it now.  Note that the ‘random’ button on the toolbar and the menu is also now enabled – just by adding a handler for the command.

这应该很容易理解。 注意,我可以直接基于SQL命令创建记录集,它不必是存储在数据库中的表或查询。 您应该现在就可以进行编译和测试。 请注意,工具栏和菜单上的“随机”按钮现在也已启用-仅通过为命令添加处理程序即可。

You might see it is messy code to convert the field values returned (as variants) into variables you can use.  I’ll leave it up to you to write a class to hide that messy code for you.  (eg. int COpenVar::GetString(CDaoRecordset* pSet, LPTSTR pszFieldName, CString& szResult) or similar)

您可能会发现将返回的字段值(作为变体)转换为可以使用的变量是很麻烦的代码。 我将由您自己编写一个类来为您隐藏该混乱代码。 (例如int COpenVar :: GetString(CDaoRe cordset * pSet,LPTSTR pszFieldName,CString&szResult)或类似)

How can we get games into the database?  Well we can create a new game and I haven’t said anything about the ‘lock’ button yet.  What if we enter the start numbers onto a new game, then ‘lock’ it as a game and at the same time add it to the database – sound reasonable?  Let's do it.

我们如何将游戏导入数据库? 好了,我们可以创建一个新游戏,但我还没有对“锁定”按钮说什么。 如果我们将起始编号输入到新游戏中,然后将其“锁定”为游戏并将其添加到数据库中,这听起来合理吗? 我们开始做吧。

Using the resource editor (as above) add a handler OnEditLock for the ‘lock’ menu item to the document.  This enables the ‘lock’ button but there will be times when we don’t want to lock a game for example when we load a game from the database.  Back to the resource editor add another event handler for the menu item – this time the UPDATE_COMMAND_UI type and call it OnUpdateEditLock.  We want the lock button to be available for only a new game – when the m_arGame variable just contains zeroes.

使用资源编辑器(如上所述),将“锁定”菜单项的处理程序OnEditLock添加到文档中。 这样可以启用“锁定”按钮,但是有时我们不想锁定游戏,例如,当我们从数据库加载游戏时。 回到资源编辑器,为菜单项添加另一个事件处理程序–这次是UPDATE_COMMAND_UI类型,并将其称为OnUpdateEditLock。 当m_arGame变量仅包含零时,我们希望锁定按钮仅可用于新游戏。

void CSudokuDoc::OnUpdateEditLock(CCmdUI *pCmdUI)
{
    char* pCell = m_arGame;
    while(*pCell != 0)
    {
        if(*pCell != '0')
        {
            pCmdUI->Enable(false);
            return;
        }
        pCell++;
    }
    pCmdUI->Enable(true);
}

Not so elegant code but it does work.  We assign a pointer to the start of the block containing the game information and loop through it until we find the terminating zero or a non ‘0’ number.  (Remember that 0 is not the same as ‘0’.)  If you compile and run you should find the lock button/menu is enabled when there is a new game and disabled when one loads a game from disc or database.  If one is enabled and the other is disabled then check the properties of the menu item and toolbar button in the resource editor – typically you will find that they have a different ID assigned to them.  Note how simple it is to provide visual updates of toolbar and menu in an SDI (or MDI).  Dialog based application require rather more work, this automatic updating is not enabled by default.

并不是那么优雅的代码,但是它确实起作用。 我们为包含游戏信息的块的开头分配一个指针,并循环遍历,直到找到终止的零或非0的数字。 (请记住,0与“ 0”不同。)如果编译并运行,则应该在有新游戏时启用锁定按钮/菜单,而在从光盘或数据库中加载游戏时禁用该按钮/菜单。 如果启用了一个而禁用了另一个,则在资源编辑器中检查菜单项和工具栏按钮的属性–通常,您会发现为它们分配了不同的ID。 请注意,在SDI(或MDI)中提供工具栏和菜单的可视更新是多么简单。 基于对话框的应用程序需要做更多的工作,默认情况下不启用此自动更新。

We have the visual update, now to save to the database.  As part of the save to disc routines we already have a line of code to help update the document.  This is the first attempt at updating the database

我们有视觉更新,现在保存到数据库。 作为保存到磁盘例程的一部分,我们已经有一行代码来帮助更新文档。 这是第一次更新数据库

void CSudokuDoc::OnEditLock()
{
    //tell the view to store the 'game' into the document
    UpdateAllViews(NULL, eSaveGame, NULL);

    //Now update the database
    CDaoRecordset RS(m_pDB);
    RS.Open(AFX_DAO_USE_DEFAULT_TYPE, "SELECT * FROM Games WHERE (ID=0)");
    RS.AddNew();
    RS.SetFieldValue("GameDetail", m_arGame);
    RS.Update();
    RS.Close();
}

Minor point – note we have used a ‘*’ in the SELECT statement.  This means all fields – this is a shortcut but it can be abused.  If your table has 50 fields this returns the contents of all 50 however if you only needed two fields then it has a lot of extra network traffic.  

次要点–请注意,我们在SELECT语句中使用了'*'。 这意味着所有字段-这是一个快捷方式,但可以被滥用。 如果您的表有50个字段,这将返回所有50个字段的内容,但是,如果您只需要两个字段,那么它将有很多额外的网络流量。

You can try to update the database now, in fact do so and we will do something really silly, we will fix it later in the article.

您现在可以尝试更新数据库,实际上是这样做,我们将做一些非常愚蠢的事情,我们将在本文后面修复。

Compile and run, now before entering any number press the ‘lock’ button, now do it again and you should see the following:

编译并运行,现在在输入任何数字之前,先按“锁定”按钮,然后再次执行此操作,您应该看到以下内容:

Error message

The first ‘lock’ did work and saved a completely blank game into the database (hmmm!), the second then attempted to create a new record again with a blank game.  The field in the table with the game details is defined as indexed with no duplicates allowed – hence the messagebox.  The first problem – lets leave that up to the user, we will ask for confirmation (only one number could have been entered, or two…., lots of cases to check for in code and not easy to check for either).  For the second save we either ignore this error or check before saving.

第一个“锁”确实起作用,并将一个完全空白的游戏保存到数据库中(hmmm!),然后第二个尝试使用空白游戏再次创建新记录。 包含游戏详细信息的表中的字段被定义为索引, 不允许重复 -因此为消息框。 第一个问题–让用户自己决定,我们将要求您确认(只能输入一个数字,或输入两个…..很多情况需要在代码中检查,而又不容易检查)。 对于第二次保存,我们要​​么忽略此错误,要么在保存之前检查。

Go to the resource view, open the string resources section and the string table.  At the bottom click on the blank line – you should see something like IDS_STRING129 in the first column appear.  We can select that and overtype - IDS_QRY_SAVE_TO_DB.  Then in the third column we provide our question eg. Is this new game completely entered, no undo possibilities.

转到资源视图,打开字符串资源部分和字符串表。 在底部单击空白行–您应该在第一栏中看到类似IDS_STRING129的内容。 我们可以选择它并改写-IDS_QRY_SAVE_TO_DB。 然后,在第三列中,例如,我们提供问题。 这款新游戏是否已完全进入,没有撤销的可能性。

In the OnEditLock function we modify it to provide the prompt:

在OnEditLock函数中,我们对其进行修改以提供提示:

void CSudokuDoc::OnEditLock()
{
    CString s;
    s.LoadString(IDS_QRY_SAVE_TO_DB);
    if(AfxMessageBox(s, MB_YESNO | MB_ICONQUESTION) != IDYES)
        return;

We could have put the text of the question here but you see one can load it directly from a resource ID.  (Actually this is too much code - the AfxMessageBox copes with the resource ID itself.  Here we use the CString variable to demonstrate how to get a resource string into code).  Using a resource string has some advantages.  What happens if one uses the same prompt in a number of locations – well it is lots of copies to modify (hard coded) or just one (resource) – your choice.  Your app should also support multiple languages?  No problems, just change the resource string (the resource editor supports multiple language versions for each individual resource).  This means no if statement blocks in the code to select which prompt to use.  (Now enter the text you want into the string resource, I have: Do you want to save this game into the database?)

我们可以将问题的文本放在这里,但是您看到可以直接从资源ID加载它。 (实际上,这是太多代码了-AfxMessageBox处理资源ID本身。在这里,我们使用CString变量来演示如何将资源字符串转换为代码。) 使用资源字符串具有一些优点。 如果您在多个位置使用相同的提示会发生什么情况-那么您可以选择修改(硬编码)或仅复制一个(资源)很多副本。 您的应用还应该支持多种语言? 没问题,只需更改资源字符串(资源编辑器为每个单独的资源支持多种语言版本)。 这意味着没有if语句在代码中阻塞以选择要使用的提示。 (现在,将所需的文本输入到字符串资源中,我有:

The second problem – duplicate entries.  We put the update call into a try….catch block.  A try...catch block is useful for making code more robust, is something fails one can allow the application to continue and take appropriate action.

第二个问题–重复的条目。 我们将更新调用放入try..catch块中。 try ... catch块对于使代码更健壮很有用,如果发生某些错误,则可以允许应用程序继续执行适当的操作。

    try
    {
        RS.Update();
    }
    catch(CDaoException* pe)
    {
        if(pe->m_pErrorInfo->m_lErrorCode != 3022)
        {
            AfxMessageBox(pe->m_pErrorInfo->m_strDescription, MB_ICONEXCLAMATION);
        }
        pe->Delete();
    }

This is error (exception) handling and trapping.  Here we attempt to call a function (Update) that will throw an exception and the code block will catch that exception.  We can check information in the exception – eg. the error code, in this case the code 3022 means a duplicate value being entered.  Note we display the error message if it is any other than our expected one.  For this case I don’t supply our own warning about a duplicate because the user has attempted to add this game to the database, it failed because it is already there – is the user interested in that?

这是错误(异常)处理和陷阱。 在这里,我们尝试调用一个函数(更新),该函数将引发异常,并且代码块将捕获该异常。 我们可以检查信息,例如- 错误代码,在这种情况下,代码3022表示输入了重复值。 请注意,如果不是我们期望的错误消息,我们将显示错误消息。 在这种情况下,我不会提供有关复制的警告,因为用户试图将游戏添加到数据库中,但由于该游戏已经存在而失败了–用户对此感兴趣吗?

How do I know the error code?  Simple, I put a breakpoint and attempted to add a duplicate then looked at what the value in the exception was.

我怎么知道错误代码? 很简单,我设置了一个断点并尝试添加一个副本,然后查看异常中的值是什么。

When we load a random game we display the ID of that game.  Now we have added a new game to the database we ought to be consistent and now display that information.  Simple I hear you say, all we need is the following after the update of the recordset

当我们加载随机游戏时,我们会显示该游戏的ID。 现在,我们已经将一个新游戏添加到数据库中,我们应该保持一致并显示该信息。 很简单,我听到您说,更新记录集后,我们需要做的只是以下内容

    COleVariant var = RS.GetFieldValue("ID");

Try it – unfortunately you get an error about no current record.  This is a feature of the DAO recordset that the AddNew…Update does not remain on the new record.  We need to use something called bookmarks

尝试一下-不幸的是,您收到关于当前没有记录的错误。 这是DAO记录集的一项功能,即AddNew…Update不会保留在新记录中。 我们需要使用一种称为书签的东西

    COleVariant varBkmark = RS.GetLastModifiedBookmark();
    RS.SetBookmark(varBkmark);
    COleVariant var = RS.GetFieldValue("ID");
    m_szInfo.Format("ID: %ld", var.lVal);

    POSITION pos = GetFirstViewPosition();
    static_cast<CMainFrame*>(GetNextView(pos)->GetParentFrame())->DisplayStatusInfo(GetInfo());

We retrieve a bookmark (an identifier) of the last record modified, then we position the set onto that record – now we can get the ID of the new record.  As this code is running in the document and the display is on the status bar we have to go a little roundabout route to display it – by looking at the views attached to a document.  We also need to #include the header for the MainFrame for this to work.

我们检索最后修改的记录的书签(标识符),然后将集合放置到该记录上-现在我们可以获取新记录的ID。 由于此代码正在文档中运行并且显示在状态栏上,因此我们必须走一些回旋路线才能显示它-通过查看文档所附的视图。 我们还需要#include MainFrame的标头以使其正常工作。

So the complete OnEditLock is as follows:

因此完整的OnEditLock如下所示:

void CSudokuDoc::OnEditLock()
{
    CString s;
    s.LoadString(IDS_QRY_SAVE_TO_DB);
    if(AfxMessageBox(s, MB_YESNO | MB_ICONQUESTION) != IDYES)
        return;

    //tell the view to store the 'game' into the document
    UpdateAllViews(NULL, eSaveGame, NULL);

    //Now update the database
    CDaoRecordset RS(m_pDB);
    RS.Open(AFX_DAO_USE_DEFAULT_TYPE, "SELECT * FROM Games WHERE (ID=0)");
    RS.AddNew();
    RS.SetFieldValue("GameDetail", m_arGame);
    try
    {
        RS.Update();
        COleVariant varBkmark = RS.GetLastModifiedBookmark();
        RS.SetBookmark(varBkmark);
        COleVariant var = RS.GetFieldValue("ID");
        m_szInfo.Format("ID: %ld", var.lVal);

        POSITION pos = GetFirstViewPosition();
        static_cast<CMainFrame*>(GetNextView(pos)->GetParentFrame())->DisplayStatusInfo(GetInfo());
    }
    catch(CDaoException* pe)
    {
        if(pe->m_pErrorInfo->m_lErrorCode != 3022)
        {
            AfxMessageBox(pe->m_pErrorInfo->m_strDescription, MB_ICONEXCLAMATION);
        }
        pe->Delete();
    }
    RS.Close();
}

Finally let’s implement the ‘Delete Game’ from the edit menu – we might have a game saved by mistake.  I don’t need to tell you in detail how to add the handler and update handler for the menu item do I?

最后,让我们从编辑菜单中实施“删除游戏”-我们可能会误保存游戏。 我不需要详细告诉您如何为菜单项添加处理程序和更新处理程序吗?

void CSudokuDoc::OnUpdateEditDeletegame(CCmdUI *pCmdUI)
{
    // Only required if we have loaded a game from the Database
    pCmdUI->Enable(m_szInfo.Left(2).CompareNoCase("ID") == 0);
}

void CSudokuDoc::OnEditDeletegame()
{
    CString s(m_szInfo.Mid(4));
    CString szSQL;
    szSQL.Format("DELETE * FROM Games WHERE (ID=%s)", s);
    m_pDB->Execute(szSQL);
    
    AfxGetMainWnd()->SendMessage(WM_COMMAND, MAKEWPARAM(ID_FILE_PLAYRANDOM, 0), NULL);
}

We know the format of the info string so we can just extract the ID of the current game simply.  Now that is used in a command to remove a record from the table by using this ID to identify it.  (This ought to be in a try…catch block but for simplicity we are not doing that).  Finally we display a random game by faking a menu event with the WM_COMMAND message to the main application window.

我们知道信息字符串的格式,因此我们可以简单地提取当前游戏的ID。 现在,在命令中使用该ID可以通过使用此ID来标识它来将其删除。 (这应该在try ... catch块中,但是为简单起见,我们不这样做)。 最后,我们通过伪造带有WM_COMMAND消息的菜单事件到主应用程序窗口来显示随机游戏。

This function ought to display a confirmation warning – but you know how to do that by now, also what happens if you delete the last game in the database?  A small exercise for you (hint: see IsBOF and IsEOF in help).

此功能应该显示一个确认警告-但是您现在知道该怎么做,如果删除数据库中的最后一个游戏,还会发生什么? 一个适合您的小练习(提示:请参见IsBOF和IsEOF的帮助)。

结论: (Conclusion:)

We have been working with a database (Microsoft Access).

我们一直在使用数据库(Microsoft Access)。

We have seen how to use random numbers.

我们已经看到了如何使用随机数。

We have implemented simple error handling with try...catch blocks.

我们使用try ... catch块实现了简单的错误处理。

We have a robust technique to find where the exe file is located on disc.

我们拥有一种可靠的技术来查找exe文件在光盘上的位置。

Click here for the source code for this article单击此处获取本文的源代码

Previous article in the series is here:  Sudoku in MFC: Part 7

该系列中的上一篇文章在这里: MFC中的Sudoku:第7部分

There we provided hints for the cells, worked with a templated collection and a nested class.  We also performed some simple debugging to find and correct an error.

在那里,我们提供了有关单元格的提示,并使用了模板化集合和嵌套类。 我们还执行了一些简单的调试,以查找并纠正错误。

Next article in the series is here:  Sudoku in MFC: Part 9

该系列的下一篇文章在这里: MFC中的Sudoku:第9部分

Here we will be creating a class to implement a simple stack for an undo mechanism.

在这里,我们将创建一个类来为撤消机制实现一个简单的堆栈。

Two points to bear in mind.

需要牢记两点。

You may use the code but you are not allowed to distribute the resulting application either for free or for a reward (monetary or otherwise).  At least not without my express permission.

您可以使用代码,但不能免费或以奖励(货币或其他方式)分发结果应用程序。 至少没有我的明确许可。

I will perform some things to demonstrate a point – it is not to be taken as that meaning it is a ‘best’ practice, in fact an alternative might be simpler and suitable.  Some points in the code would even be called poor design and a possible source of errors.

我将做一些事情来说明一个观点–不能认为它是“最佳”实践,实际上,另一种选择可能更简单,更合适。 代码中的某些要点甚至被称为不良设计和可能的错误源。

翻译自: https://www.experts-exchange.com/articles/3828/Sudoku-a-complete-MFC-application-Part-8.html

sudoku me

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值