这次主要增加了一个复盘的功能,另外代码也改了很多地方,现在做一个集中的说明。
1.复盘功能
1.1复盘控件
复盘时需要有一个前进与后退的按钮,另外还需要有一个比例滑块,通过拉动滑块来快速跳转到某一步,最后显示“步数”文字的label也是一个控件。我们把这几个控件组合在一起,都放在一个pJunqi->step_fixed里,这样只要隐藏pJunqi->step_fixed就可以把所有的控件都隐藏。
由于前进与后退按钮要表明方向,但是在GTK中没找到方向按钮,所以通过以下按钮图片素材
截取图片上的按钮贴在按钮控件上作为前进与后退的按钮。
接下来是新建一个滑块,新建滑块前需要一个adjustment控件绑定在滑块上,adjustment可以设定滑块的取值范围,注意滑块一定要设置长和宽,否则显示的滑块将不能拉动。
GtkAdjustment *adj;
//只需关注前3个参数,分别为当前值,最小值和最大值
adj = gtk_adjustment_new (0, 0, 100, 1, 1, 0);
slider = gtk_scale_new (GTK_ORIENTATION_HORIZONTAL, adj);
//比例控件以后会用到,调入复盘时设定最大步数为滑块的最大值
pJunqi->slider_adj = adj;
//设置步数改变时的回调函数
g_signal_connect (adj, "value-changed",
G_CALLBACK (on_slider_value_change),
pJunqi);
//必须,否则滑块将不能拉动
gtk_widget_set_size_request(slider, length,20);
//小数点位数为0
gtk_scale_set_digits(GTK_SCALE (slider), 0);
gtk_fixed_put(GTK_FIXED(fixed), slider, 0, 50);
gtk_widget_show (slider);
最后复盘界面的整体效果如下
1.2打开和保存复盘
首先在菜单中增加打开复盘和保存复盘2个按钮
gBoard.open_menu = SetMenuItem(menu, "打开复盘", event_handle, "open");
gBoard.save_menu = SetMenuItem(menu, "保存复盘", event_handle, "save");
在对战模式下,打开复盘按钮是灰色的,在新建和复盘模式下,保存复盘按钮是灰色的,相关函数如下:
//设置打开菜单灰色
gtk_widget_set_sensitive(gBoard.open_menu, FALSE);
//激活保存按钮
gtk_widget_set_sensitive(gBoard.save_menu, TRUE);
点击菜单后将弹出一个对话框,使用tk_file_chooser_native_new函数,如果是打开选择GTK_FILE_CHOOSER_ACTION_OPEN参数,如果是保存则选择GTK_FILE_CHOOSER_ACTION_SAVE选项
native = gtk_file_chooser_native_new ("Open File",
NULL,
GTK_FILE_CHOOSER_ACTION_OPEN,
"_Open",
"_Cancel");
复盘格式如下
偏移 | 长度 | 说明 |
---|---|---|
0 | 4 | 复盘标识,固定0x57,0x04,0,0 |
4 | 4 | 复盘总步数 |
8 - ? | - | 每4字节为一步,前2字节为起始坐标,后2字节为目标坐标 |
棋盘是一个17*17网格,最右边的横坐标为0,最上面的纵坐标为0。
每移动一步都会记录在pJunqi->aReplay数组里,最后保存总的步数,在点击保存按钮后,将pJunqi->aReplay保存到指定的文件,pJunqi->iReOfst记录文件的末尾地址
if (response_id == GTK_RESPONSE_ACCEPT)
{
name = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER (native));
ConvertFilename(name);
fd = open(name, O_RDWR|O_CREAT, 0600);
assert( pJunqi->iReOfst>=MOVE_OFFSET );
iTolalStep = (pJunqi->iReOfst-MOVE_OFFSET)/4;
SetReplayData(pJunqi, (u8*)&iTolalStep, 4, 4);
OsWrite(fd, pJunqi->aReplay, pJunqi->iReOfst, 0);
}
打开复盘时先要校对复盘文件标识符,如果正确则导入复盘文件,初始化棋盘,初始化布阵等等相关初始化操作
OsRead(fd, aBuf, PAGE_SIZE, 0);
if( memcmp(aBuf, aMagic, 4)==0 )
{
pJunqi->bReplay = 1;
//导入复盘文件
memcpy(pJunqi->aReplay, aBuf, PAGE_SIZE);
//初始化棋盘
ReSetChessBoard(pJunqi);
//导入布阵
LoadReplayLineup(pJunqi);
//显示比例滑块
ShowReplaySlider(pJunqi);
//设定最大步数为滑块上限
int max_step = *(int *)(&pJunqi->aReplay[4]);
gtk_adjustment_set_upper(pJunqi->slider_adj, max_step);
pJunqi->bStart = 1;
pJunqi->bStop = 1;
pJunqi->iRpStep = 0;
}
1.3播放复盘
播放复盘时通过pJunqi->iRpStep记录当前的步数,如果pJunqi->iRpStep的值改变了,则棋盘从初始开始一直播放到pJunqi->iRpStep这一步,再显示最终的局面。
for(i=0; i<pJunqi->iRpStep; i++)
{
获取每一步的起始和目标坐标,根据坐标得到棋子pSrc和pDst的信息
然后和正常行棋一样,比较2个棋子的大小,再做最后的移动
int type;
type = CompareChess(pSrc, pDst);
PlayResult(pJunqi, pSrc, pDst, type);
}
点击前进和后退时会改变滑块控件的值,这时会触发回调函数,在回调函数里调用ShowReplayStep()播放复盘
if( strcmp(zDir,"left" )==0 )
{
if( pJunqi->iRpStep>0 )
{
gtk_adjustment_set_value(pJunqi->slider_adj, pJunqi->iRpStep-1);
}
}
else if( strcmp(zDir,"right" )==0 )
{
if( pJunqi->iRpStep<max_step )
{
pJunqi->iRpStep++;
gtk_adjustment_set_value(pJunqi->slider_adj, pJunqi->iRpStep);
ShowReplayStep(pJunqi, 1);
}
}
只有在下一步时才播放声音,其他时候不播放声音
static void
on_slider_value_change (GtkAdjustment *adjustment,
gpointer data)
{
... ...
if( value!=pJunqi->iRpStep )
{
pJunqi->iRpStep = value;
ShowReplayStep(pJunqi, 0);
pJunqi->sound_type = 0;
}
}
2.其他功能
2.1时间功能
先创建一个100ms的定时器
g_timeout_add(100, (GSourceFunc)time_event, pJunqi);
每方下棋的时间为30s,30s到达后算一次超时,轮到下一家下。
static int tick = 300;
if( tick==0 )
{
AddEventToReplay(pJunqi, JUMP_EVENT, pJunqi->eTurn);
IncJumpCnt(pJunqi, pJunqi->eTurn);
ChessTurn(pJunqi);
now_dir = pJunqi->eTurn;
tick = 300;
}
如超时或跳过5次将被判定为投降
if( cntJump==5 )
{
SendSoundEvent(pJunqi,DEAD);
DestroyAllChess(pJunqi, iDir);
HideJumpButton(iDir);
}
正常下棋后,重新显示30s
if( now_dir!=pJunqi->eTurn )
{
now_dir = pJunqi->eTurn;
tick = 300;
}
以下是显示时间的函数,通过sapn标签来改变字体的大小
void SetTimeStr(char *label_str, int time)
{
char str[100] = "<span foreground=\"#FFFF00\" font='16'>";
int szStr;
memcpy(label_str,str,strlen(str));
sprintf(label_str+strlen(str),"%02d",time);
szStr = strlen(label_str);
memcpy(label_str+szStr,"</span>",8);
}
SetTimeStr(label_str, (tick+9)/10);
gtk_label_set_markup(GTK_LABEL(pJunqi->pTimeLabel),label_str);
最后效果如下
另外还可以通过设定里的暂停菜单来停止计时
2.2跳过和投降按钮
在跳过或行棋时会改变下棋者,这里要考虑阵亡的情况,如果出现阵亡则紧接着跳到下一家
void ChessTurn(Junqi *pJunqi)
{
pJunqi->eTurn = (pJunqi->eTurn+1)%4;
//下家阵亡
if( pJunqi->aInfo[pJunqi->eTurn].bDead )
{
pJunqi->eTurn = (pJunqi->eTurn+1)%4;
//下下家阵亡
if( pJunqi->aInfo[pJunqi->eTurn].bDead )
{
pJunqi->eTurn = (pJunqi->eTurn+1)%4;
}
}
//跳过时取消选中的棋子
pJunqi->bSelect = 0;
gtk_widget_hide(pJunqi->whiteRectangle[0]);
gtk_widget_hide(pJunqi->whiteRectangle[1]);
}
点击投降时需要弹出一个确认对话框
dialog = gtk_dialog_new_with_buttons ("Interactive Dialog",
GTK_WINDOW (pJunqi->window),
GTK_DIALOG_MODAL| GTK_DIALOG_DESTROY_WITH_PARENT,
("_OK"),
GTK_RESPONSE_OK,
"_Cancel",
GTK_RESPONSE_CANCEL,
NULL);
content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
label = gtk_label_new_with_mnemonic ("您确定投降吗");
gtk_box_pack_start (GTK_BOX (content_area), label, FALSE, FALSE, 0);
gtk_widget_show(label);
response = gtk_dialog_run (GTK_DIALOG (dialog));
确定后将会销毁这一家的所有棋子
if (response == GTK_RESPONSE_OK)
{
SendSoundEvent(pJunqi,DEAD);
DestroyAllChess(pJunqi, iDir);
AddEventToReplay(pJunqi, SURRENDER_EVENT, iDir);
ChessTurn(pJunqi);
HideJumpButton(iDir);
}