input输入框输入缓存值后背景问题,清除黄色背景

在项目中遇到输入框只要输入缓存值就会变成黄色背景,手动输入就是正常,只需要添加下面一行代码即可解决哟,拿走不谢,?

input:-webkit-autofill { 
	-webkit-box-shadow: 0 0 0 1000px white inset !important;
	}
// JScript 檔 // 主调用函数是 setday(this,[object])和setday(this),[object]是控件输出的控件名,举两个例子: // 一、<input name=txt><input type=button value=setday onclick="setday(this,document.all.txt)"> // 二、<input onfocus="setday(this)"> var bMoveable = true; var strFrame; document.writeln('<iframe id=endDateLayer frameborder=0 width=162 height=211 style="position: absolute; z-index: 9998; display: none"></iframe>'); strFrame = '<style>'; strFrame += 'INPUT.button{BORDER-RIGHT: #63A3E9 1px solid;BORDER-TOP: #63A3E9 1px solid;BORDER-LEFT: #63A3E9 1px solid;'; strFrame += 'BORDER-BOTTOM: #63A3E9 1px solid;BACKGROUND-COLOR: #63A3E9;font-family:宋体;}'; strFrame += 'TD{FONT-SIZE: 9pt;font-family:宋体;}'; strFrame += '</style>'; strFrame += '<scr' + 'ipt>'; strFrame += 'var datelayerx,datelayery;'; strFrame += 'var bDrag;'; strFrame += 'function document.onmousemove()'; strFrame += '{if(bDrag && window.event.button==1)'; strFrame += ' {var DateLayer=parent.document.all.endDateLayer.style;'; strFrame += ' DateLayer.posLeft += window.event.clientX-datelayerx;'; strFrame += ' DateLayer.posTop += window.event.clientY-datelayery;}}'; strFrame += 'function DragStart()'; strFrame += '{var DateLayer=parent.document.all.endDateLayer.style;'; strFrame += ' datelayerx=window.event.clientX;'; strFrame += ' datelayery=window.event.clientY;'; strFrame += ' bDrag=true;}'; strFrame += 'function DragEnd(){'; strFrame += ' bDrag=false;}'; strFrame += '</scr' + 'ipt>'; strFrame += '<div style="z-index:9999;position: absolute; left:0; top:0;" onselectstart="return false">'; strFrame += '<span id=tmpSelectYearLayer style="z-index: 9999;position: absolute;top: 3; left: 19;display: none"></span>'; strFrame += '<span id=tmpSelectMonthLayer style="z-index: 9999;position: absolute;top: 3; left: 78;display: none"></span>'; strFrame += '<span id=tmpSelectHourLayer style="z-index: 9999;position: absolute;top: 188; left: 35px;display: none"></span>'; strFrame += '<span id=tmpSelectMinuteLayer style="z-index:9999;position:absolute;top: 188; left: 77px;display: none"></span>'; strFrame += '<span id=tmpSelectSecondLayer style="z-index:9999;position:absolute;top: 188; left: 119px;display: none"></span>'; strFrame += '<table border=1 cellspacing=0 cellpadding=0 width=142 height=160 bordercolor=#63A3E9 bgcolor=#63A3E9 >'; strFrame += ' <tr><td width=142 height=23 bgcolor=#FFFFFF>'; strFrame += ' <table border=0 cellspacing=1 cellpadding=0 width=158 height=23>'; strFrame += ' <tr align=center >'; strFrame += ' <td width=16 align=center bgcolor=#63A3E9 style="font-size:12px;cursor: hand;color: #ffffff"; display: none'; strFrame += ' onclick="parent.meizzPrevM()" title="向前翻 1 月" ><b ><</b></td>'; strFrame += ' <td width=60 align="center" bgcolor="#63A3E9" style="font-size:12px;cursor:hand" '; strFrame += ' onmouseover="style.backgroundColor=\'#aaccf3\'"'; strFrame += ' onmouseout="style.backgroundColor=\'#63A3E9\'" '; strFrame += ' onclick="parent.tmpSelectYearInnerHTML(this.innerText.substring(0,4))" '; strFrame += ' title="點擊這裡選擇年份"><span id=meizzYearHead></span></td>'; strFrame += ' <td width=48 align="center" style="font-size:12px;font-color: #ffffff;cursor:hand" '; strFrame += ' bgcolor="#63A3E9" onmouseover="style.backgroundColor=\'#aaccf3\'" '; strFrame += ' onmouseout="style.backgroundColor=\'#63A3E9\'" '; strFrame += ' onclick="parent.tmpSelectMonthInnerHTML(this.innerText.length==3?this.innerText.substring(0,1):this.innerText.substring(0,2))"'; strFrame += ' title="點擊這裡選擇月份"><span id=meizzMonthHead ></span></td>'; strFrame += ' <td width=16 bgcolor=#63A3E9 align=center style="font-size:12px;cursor: hand;color: #ffffff" '; strFrame += ' onclick="parent.meizzNextM()" title="向后翻 1 月" ><b >></b></td>'; strFrame += ' </tr>'; strFrame += ' </table></td></tr>'; strFrame += ' <tr><td width=142 height=18 >'; strFrame += ' <table border=0 cellspacing=0 cellpadding=2 bgcolor=#63A3E9 ' + (bMoveable ? 'onmousedown="DragStart()" onmouseup="DragEnd()"' : ''); strFrame += ' BORDERCOLORLIGHT=#63A3E9 BORDERCOLORDARK=#FFFFFF width=140 height=20 style="cursor:' + (bMoveable ? 'move' : 'default') + '">'; strFrame += ' <tr><td style="font-size:12px;color:#ffffff" width=20> 日</td>'; strFrame += '<td style="font-size:12px;color:#FFFFFF" > 一</td><td style="font-size:12px;color:#FFFFFF"> 二</td>'; strFrame += '<td style="font-size:12px;color:#FFFFFF" > 三</td><td style="font-size:12px;color:#FFFFFF" > 四</td>'; strFrame += '<td style="font-size:12px;color:#FFFFFF" > 五</td><td style="font-size:12px;color:#FFFFFF" > 六</td></tr>'; strFrame += '</table></td></tr>'; strFrame += ' <tr ><td width=142 height=120 >'; strFrame += ' <table border=1 cellspacing=2 cellpadding=2 BORDERCOLORLIGHT=#63A3E9 BORDERCOLORDARK=#FFFFFF bgcolor=#fff8ec width=140 height=120 >'; var n = 0; for (j = 0; j < 5; j++) { strFrame += ' <tr align=center >'; for (i = 0; i < 7; i++) { strFrame += '<td width=20 height=20 id=meizzDay' + n + ' style="font-size:12px" onclick=parent.meizzDayClick(this.innerText,0)></td>'; n++; } strFrame += '</tr>'; } strFrame += ' <tr align=center >'; for (i = 35; i < 37; i++) strFrame += '<td width=20 height=20 id=meizzDay' + i + ' style="font-size:12px" onclick="parent.meizzDayClick(this.innerText,0)"></td>'; strFrame += ' <td colspan=5 align=right style="color:#1478eb"><span onclick="parent.setNull()" style="font-size:12px;cursor: hand"'; strFrame += ' onmouseover="style.color=\'#ff0000\'" onmouseout="style.color=\'#1478eb\'" title="将日期置空">置空</span> <span onclick="parent.meizzToday()" style="font-size:12px;cursor: hand"'; strFrame += ' onmouseover="style.color=\'#ff0000\'" onmouseout="style.color=\'#1478eb\'" title="当前日期时间">當前</span> <span style="cursor:hand" id=evaAllOK onmouseover="style.color=\'#ff0000\'" onmouseout="style.color=\'#1478eb\'" onclick="parent.closeLayer()" title="關閉日曆">確定 </span></td></tr>'; strFrame += ' </table></td></tr><tr visible="false"><td >'; strFrame += ' <table border=0 cellspacing=1 cellpadding=0 width=100% bgcolor=#FFFFFF height=22 >'; strFrame += ' <tr bgcolor="#63A3E9"><td id=bUseTimeLayer width=30 style="cursor:hand" title="點擊這裡啟用/禁用時間"'; strFrame += ' onmouseover="style.backgroundColor=\'#aaccf3\'" align=center onmouseout="style.backgroundColor=\'#63A3E9\'"'; strFrame += ' onclick="parent.UseTime(this)">'; strFrame += ' <span></span></td>'; strFrame += ' <td style="cursor:hand" onclick="parent.tmpSelectHourInnerHTML(this.innerText.length==3?this.innerText.substring(0,1):this.innerText.substring(0,2))"'; strFrame += ' onmouseover="style.backgroundColor=\'#aaccf3\'" onmouseout="style.backgroundColor=\'#63A3E9\'"'; strFrame += ' title="點擊這裡選擇時間" align=center width=42>'; strFrame += ' <span id=meizzHourHead></span></td>'; strFrame += ' <td style="cursor:hand" onclick="parent.tmpSelectMinuteInnerHTML(this.innerText.length==3?this.innerText.substring(0,1):this.innerText.substring(0,2))"'; strFrame += ' onmouseover="style.backgroundColor=\'#aaccf3\'" onmouseout="style.backgroundColor=\'#63A3E9\'"'; strFrame += ' title="點擊這裡選擇時間" align=center width=42>'; strFrame += ' <span id=meizzMinuteHead></span></td>'; strFrame += ' <td style="cursor:hand" onclick="parent.tmpSelectSecondInnerHTML(this.innerText.length==3?this.innerText.substring(0,1):this.innerText.substring(0,2))"'; strFrame += ' onmouseover="style.backgroundColor=\'#aaccf3\'" onmouseout="style.backgroundColor=\'#63A3E9\'"'; strFrame += ' title="點擊這裡選擇時間" align=center width=42>'; strFrame += ' <span id=meizzSecondHead></span></td>'; strFrame += ' </tr></table></td></tr></table></div>'; window.frames.endDateLayer.document.writeln(strFrame); window.frames.endDateLayer.document.close(); //解决ie进度条不结束的问题 //==================================================== WEB 页面显示部分 ====================================================== var outObject; var outButton; //点击的按钮 var outDate = ""; //存放对象的日期 var bUseTime = false; //是否使用时间 var odatelayer = window.frames.endDateLayer.document.all; //存放日历对象 var odatelayer = window.endDateLayer.document.all; //odatelayer.bUseTimeLayer.innerText="NO"; bImgSwitch(); odatelayer.bUseTimeLayer.innerHTML = bImg; function setday(tt, obj) //主调函数 { if (arguments.length > 2) { alert("对不起!传入本控件的参数太多!"); return; } if (arguments.length == 0) { alert("对不起!您没有传回本控件任何参数!"); return; } var dads = document.all.endDateLayer.style; var th = tt; var ttop = tt.offsetTop; //TT控件的定位点高 var thei = tt.clientHeight; //TT控件本身的高 var tleft = tt.offsetLeft; //TT控件的定位点宽 var ttyp = tt.type; //TT控件的类型 while (tt = tt.offsetParent) { ttop += tt.offsetTop; tleft += tt.offsetLeft; } dads.top = (ttyp == "image") ? ttop + thei : ttop + thei + 6; dads.left = tleft; outObject = (arguments.length == 1) ? th : obj; outButton = (arguments.length == 1) ? null : th; //设定外部点击的按钮 //根据当前输入框的日期显示日历的年月 var reg = /^(\d+)-(\d{1,2})-(\d{1,2})/; //不含时间 var r = outObject.value.match(reg); if (r != null) { r[2] = r[2] - 1; var d = new Date(r[1], r[2], r[3]); if (d.getFullYear() == r[1] && d.getMonth() == r[2] && d.getDate() == r[3]) { outDate = d; parent.meizzTheYear = r[1]; parent.meizzTheMonth = r[2]; parent.meizzTheDate = r[3]; } else { outDate = ""; } meizzSetDay(r[1], r[2] + 1); } else { outDate = ""; meizzSetDay(new Date().getFullYear(), new Date().getMonth() + 1); } dads.display = ''; //判断初始化时是否使用时间,非严格验证 //if (outObject.value.length>10) //{ bUseTime = true; bImgSwitch(); odatelayer.bUseTimeLayer.innerHTML = bImg; meizzWriteHead(meizzTheYear, meizzTheMonth); //} //else //{ // bUseTime=false; // bImgSwitch(); // odatelayer.bUseTimeLayer.innerHTML=bImg; // meizzWriteHead(meizzTheYear,meizzTheMonth); //} try { event.returnValue = false; } catch (e) { //此处排除错误,错误原因暂未找到。 } } var MonHead = new Array(12); //定义阳历中每个月的最大天数 MonHead[0] = 31; MonHead[1] = 28; MonHead[2] = 31; MonHead[3] = 30; MonHead[4] = 31; MonHead[5] = 30; MonHead[6] = 31; MonHead[7] = 31; MonHead[8] = 30; MonHead[9] = 31; MonHead[10] = 30; MonHead[11] = 31; var meizzTheYear = new Date().getFullYear(); //定义年的量的初始 var meizzTheMonth = new Date().getMonth() + 1; //定义月的量的初始 var meizzTheDate = new Date().getDate(); //定义日的量的初始 var meizzTheHour = new Date().getHours(); //定义小时量的初始 var meizzTheMinute = new Date().getMinutes(); //定义分钟量的初始 var meizzTheSecond = new Date().getSeconds(); //定义秒量的初始 var meizzWDay = new Array(37); //定义写日期的数组 function document.onclick() //任意点击时关闭该控件 //ie6的情况可以由下面的切换焦点处理代替 { with (window.event) { if (srcElement != outObject && srcElement != outButton) closeLayer(); } } function document.onkeyup() //按Esc键关闭,切换焦点关闭 { if (window.event.keyCode == 27) { if (outObject) outObject.blur(); closeLayer(); } else if (document.activeElement) { if (document.activeElement != outObject && document.activeElement != outButton) { closeLayer(); } } } function meizzWriteHead(yy, mm, ss) //往 head 中写入当前的年与月 { odatelayer.meizzYearHead.innerText = yy + " 年"; odatelayer.meizzMonthHead.innerText = format(mm) + " 月"; //插入当前小时、分 odatelayer.meizzHourHead.innerText = bUseTime ? (meizzTheHour + " 时") : ""; odatelayer.meizzMinuteHead.innerText = bUseTime ? (meizzTheMinute + " 分") : ""; odatelayer.meizzSecondHead.innerText = bUseTime ? (meizzTheSecond + " 秒") : ""; } function tmpSelectYearInnerHTML(strYear) //年份的下拉 { if (strYear.match(/\D/) != null) { alert("年份输入参数不是数字!"); return; } var m = (strYear) ? strYear : new Date().getFullYear(); if (m < 1000 || m > 9999) { alert("年份不在 1000 到 9999 之间!"); return; } var n = m - 50; if (n < 1000) n = 1000; if (n + 101 > 9999) n = 9974; var s = " <select name=tmpSelectYear style='font-size: 12px' " s += "onblur='document.all.tmpSelectYearLayer.style.display=\"none\"' " s += "onchange='document.all.tmpSelectYearLayer.style.display=\"none\";" s += "parent.meizzTheYear = this.value; parent.meizzSetDay(parent.meizzTheYear,parent.meizzTheMonth)'>\r\n"; var selectInnerHTML = s; for (var i = n; i < n + 101; i++) { if (i == m) { selectInnerHTML += "<option value='" + i + "' selected>" + i + "年" + "</option>\r\n"; } else { selectInnerHTML += "<option value='" + i + "'>" + i + "年" + "</option>\r\n"; } } selectInnerHTML += "</select>"; odatelayer.tmpSelectYearLayer.style.display = ""; odatelayer.tmpSelectYearLayer.innerHTML = selectInnerHTML; odatelayer.tmpSelectYear.focus(); } function tmpSelectMonthInnerHTML(strMonth) //月份的下拉 { if (strMonth.match(/\D/) != null) { alert("月份输入参数不是数字!"); return; } var m = (strMonth) ? strMonth : new Date().getMonth() + 1; var s = " <select name=tmpSelectMonth style='font-size: 12px' " s += "onblur='document.all.tmpSelectMonthLayer.style.display=\"none\"' " s += "onchange='document.all.tmpSelectMonthLayer.style.display=\"none\";" s += "parent.meizzTheMonth = this.value; parent.meizzSetDay(parent.meizzTheYear,parent.meizzTheMonth)'>\r\n"; var selectInnerHTML = s; for (var i = 1; i < 13; i++) { if (i == m) { selectInnerHTML += "<option value='" + i + "' selected>" + i + "月" + "</option>\r\n"; } else { selectInnerHTML += "<option value='" + i + "'>" + i + "月" + "</option>\r\n"; } } selectInnerHTML += "</select>"; odatelayer.tmpSelectMonthLayer.style.display = ""; odatelayer.tmpSelectMonthLayer.innerHTML = selectInnerHTML; odatelayer.tmpSelectMonth.focus(); } /**//***** 增加 小时、分钟 ***/ function tmpSelectHourInnerHTML(strHour) //小时的下拉 { if (!bUseTime) { return; } if (strHour.match(/\D/) != null) { alert("小时输入参数不是数字!"); return; } var m = (strHour) ? strHour : new Date().getHours(); var s = "<select name=tmpSelectHour style='font-size: 12px' " s += "onblur='document.all.tmpSelectHourLayer.style.display=\"none\"' " s += "onchange='document.all.tmpSelectHourLayer.style.display=\"none\";" s += "parent.meizzTheHour = this.value; parent.evaSetTime(parent.meizzTheHour,parent.meizzTheMinute);'>\r\n"; var selectInnerHTML = s; for (var i = 0; i < 24; i++) { if (i == m) { selectInnerHTML += "<option value='" + i + "' selected>" + i + "</option>\r\n"; } else { selectInnerHTML += "<option value='" + i + "'>" + i + "</option>\r\n"; } } selectInnerHTML += "</select>"; odatelayer.tmpSelectHourLayer.style.display = ""; odatelayer.tmpSelectHourLayer.innerHTML = selectInnerHTML; odatelayer.tmpSelectHour.focus(); } function tmpSelectMinuteInnerHTML(strMinute) //分钟的下拉 { if (!bUseTime) { return; } if (strMinute.match(/\D/) != null) { alert("分钟输入参数不是数字!"); return; } var m = (strMinute) ? strMinute : new Date().getMinutes(); var s = "<select name=tmpSelectMinute style='font-size: 12px' " s += "onblur='document.all.tmpSelectMinuteLayer.style.display=\"none\"' " s += "onchange='document.all.tmpSelectMinuteLayer.style.display=\"none\";" s += "parent.meizzTheMinute = this.value; parent.evaSetTime(parent.meizzTheHour,parent.meizzTheMinute);'>\r\n"; var selectInnerHTML = s; for (var i = 0; i < 60; i++) { // if (i == m) { selectInnerHTML += "<option value='"+i+"' selected>"+i+"</option>\r\n"; } // else { selectInnerHTML += "<option value='"+i+"'>"+i+"</option>\r\n"; } if (i == m) { if (i < 10) selectInnerHTML += "<option value='0" + i + "' selected>0" + i + "</option>\r\n"; else selectInnerHTML += "<option value='" + i + "' selected>" + i + "</option>\r\n"; } else { if (i < 10) selectInnerHTML += "<option value='0" + i + "'>0" + i + "</option>\r\n"; else selectInnerHTML += "<option value='" + i + "'>" + i + "</option>\r\n"; } } selectInnerHTML += "</select>"; odatelayer.tmpSelectMinuteLayer.style.display = ""; odatelayer.tmpSelectMinuteLayer.innerHTML = selectInnerHTML; odatelayer.tmpSelectMinute.focus(); } function tmpSelectSecondInnerHTML(strSecond) //秒的下拉 { if (!bUseTime) { return; } if (strSecond.match(/\D/) != null) { alert("分钟输入参数不是数字!"); return; } var m = (strSecond) ? strSecond : new Date().getMinutes(); var s = "<select name=tmpSelectSecond style='font-size: 12px' " s += "onblur='document.all.tmpSelectSecondLayer.style.display=\"none\"' " s += "onchange='document.all.tmpSelectSecondLayer.style.display=\"none\";" s += "parent.meizzTheSecond = this.value; parent.evaSetTime(parent.meizzTheHour,parent.meizzTheMinute,parent.meizzTheSecond);'>\r\n"; var selectInnerHTML = s; for (var i = 0; i < 60; i++) { // if (i == m) { selectInnerHTML += "<option value='"+i+"' selected>"+i+"</option>\r\n"; } // else { selectInnerHTML += "<option value='"+i+"'>"+i+"</option>\r\n"; } if (i == m) { if (i < 10) selectInnerHTML += "<option value='0" + i + "' selected>0" + i + "</option>\r\n"; else selectInnerHTML += "<option value='" + i + "' selected>" + i + "</option>\r\n"; } else { if (i < 10) selectInnerHTML += "<option value='0" + i + "'>0" + i + "</option>\r\n"; else selectInnerHTML += "<option value='" + i + "'>" + i + "</option>\r\n"; } } selectInnerHTML += "</select>"; odatelayer.tmpSelectSecondLayer.style.display = ""; odatelayer.tmpSelectSecondLayer.innerHTML = selectInnerHTML; odatelayer.tmpSelectSecond.focus(); } function closeLayer() //这个层的关闭 { var o = document.getElementById("endDateLayer"); if (o != null) { o.style.display = "none"; } } function showLayer() //这个层的关闭 { document.all.endDateLayer.style.display = ""; } function IsPinYear(year) //判断是否闰平年 { if (0 == year % 4 && ((year % 100 != 0) || (year % 400 == 0))) return true; else return false; } function GetMonthCount(year, month) //闰年二月为29天 { var c = MonHead[month - 1]; if ((month == 2) && IsPinYear(year)) c++; return c; } function GetDOW(day, month, year) //求某天的星期几 { var dt = new Date(year, month - 1, day).getDay() / 7; return dt; } function meizzPrevY() //往前翻 Year { if (meizzTheYear > 999 && meizzTheYear < 10000) { meizzTheYear--; } else { alert("年份超出范围(1000-9999)!"); } meizzSetDay(meizzTheYear, meizzTheMonth); } function meizzNextY() //往后翻 Year { if (meizzTheYear > 999 && meizzTheYear < 10000) { meizzTheYear++; } else { alert("年份超出范围(1000-9999)!"); } meizzSetDay(meizzTheYear, meizzTheMonth); } function setNull() { outObject.value = ''; closeLayer(); } function meizzToday() //Today Button { parent.meizzTheYear = new Date().getFullYear(); parent.meizzTheMonth = new Date().getMonth() + 1; parent.meizzTheDate = new Date().getDate(); parent.meizzTheHour = new Date().getHours(); parent.meizzTheMinute = new Date().getMinutes(); parent.meizzTheSecond = new Date().getSeconds(); var meizzTheSecond = new Date().getSeconds(); if (meizzTheMonth < 10 && meizzTheMonth.length < 2) //格式化成两位数字 { parent.meizzTheMonth = "0" + parent.meizzTheMonth; } if (parent.meizzTheDate < 10 && parent.meizzTheDate.length < 2) //格式化成两位数字 { parent.meizzTheDate = "0" + parent.meizzTheDate; } //meizzSetDay(meizzTheYear,meizzTheMonth); if (outObject) { if (bUseTime) { outObject.value = parent.meizzTheYear + "/" + format(parent.meizzTheMonth) + "/" + format(parent.meizzTheDate) + " " + format(parent.meizzTheHour) + ":" + format(parent.meizzTheMinute) //+ ":" + format(parent.meizzTheSecond); //注:在这里你可以输出改成你想要的格式 } else { outObject.value = parent.meizzTheYear + "-" + format(parent.meizzTheMonth) + "-" + format(parent.meizzTheDate); //注:在这里你可以输出改成你想要的格式 } } closeLayer(); } function meizzPrevM() //往前翻月份 { if (meizzTheMonth > 1) { meizzTheMonth-- } else { meizzTheYear--; meizzTheMonth = 12; } meizzSetDay(meizzTheYear, meizzTheMonth); } function meizzNextM() //往后翻月份 { if (meizzTheMonth == 12) { meizzTheYear++; meizzTheMonth = 1 } else { meizzTheMonth++ } meizzSetDay(meizzTheYear, meizzTheMonth); } // TODO: 整理代码 function meizzSetDay(yy, mm) //主要的写程序********** { meizzWriteHead(yy, mm); //设置当前年月的公共量为传入 meizzTheYear = yy; meizzTheMonth = mm; for (var i = 0; i < 37; i++) { meizzWDay[i] = "" }; //将显示的内容全部清空 var day1 = 1, day2 = 1, firstday = new Date(yy, mm - 1, 1).getDay(); //某月第一天的星期几 for (i = 0; i < firstday; i++) meizzWDay[i] = GetMonthCount(mm == 1 ? yy - 1 : yy, mm == 1 ? 12 : mm - 1) - firstday + i + 1 //上个月的最后几天 for (i = firstday; day1 < GetMonthCount(yy, mm) + 1; i++) { meizzWDay[i] = day1; day1++; } for (i = firstday + GetMonthCount(yy, mm); i < 37; i++) { meizzWDay[i] = day2; day2++; } for (i = 0; i < 37; i++) { var da = eval("odatelayer.meizzDay" + i) //书写新的一个月的日期星期排列 if (meizzWDay[i] != "") { //初始化边 da.borderColorLight = "#63A3E9"; da.borderColorDark = "#63A3E9"; da.style.color = "#1478eb"; if (i < firstday) //上个月的部分 { da.innerHTML = "<b><font color=#BCBABC>" + meizzWDay[i] + "</font></b>"; da.title = (mm == 1 ? 12 : mm - 1) + "月" + meizzWDay[i] + "日"; da.onclick = Function("meizzDayClick(this.innerText,-1)"); if (!outDate) da.style.backgroundColor = ((mm == 1 ? yy - 1 : yy) == new Date().getFullYear() && (mm == 1 ? 12 : mm - 1) == new Date().getMonth() + 1 && meizzWDay[i] == new Date().getDate()) ? "#5CEFA0" : "#f5f5f5"; else { da.style.backgroundColor = ((mm == 1 ? yy - 1 : yy) == outDate.getFullYear() && (mm == 1 ? 12 : mm - 1) == outDate.getMonth() + 1 && meizzWDay[i] == outDate.getDate()) ? "#84C1FF" : (((mm == 1 ? yy - 1 : yy) == new Date().getFullYear() && (mm == 1 ? 12 : mm - 1) == new Date().getMonth() + 1 && meizzWDay[i] == new Date().getDate()) ? "#5CEFA0" : "#f5f5f5"); //将选中的日期显示为凹下去 if ((mm == 1 ? yy - 1 : yy) == outDate.getFullYear() && (mm == 1 ? 12 : mm - 1) == outDate.getMonth() + 1 && meizzWDay[i] == outDate.getDate()) { da.borderColorLight = "#FFFFFF"; da.borderColorDark = "#63A3E9"; } } } else if (i >= firstday + GetMonthCount(yy, mm)) //下个月的部分 { da.innerHTML = "<b><font color=#BCBABC>" + meizzWDay[i] + "</font></b>"; da.title = (mm == 12 ? 1 : mm + 1) + "月" + meizzWDay[i] + "日"; da.onclick = Function("meizzDayClick(this.innerText,1)"); if (!outDate) da.style.backgroundColor = ((mm == 12 ? yy + 1 : yy) == new Date().getFullYear() && (mm == 12 ? 1 : mm + 1) == new Date().getMonth() + 1 && meizzWDay[i] == new Date().getDate()) ? "#5CEFA0" : "#f5f5f5"; else { da.style.backgroundColor = ((mm == 12 ? yy + 1 : yy) == outDate.getFullYear() && (mm == 12 ? 1 : mm + 1) == outDate.getMonth() + 1 && meizzWDay[i] == outDate.getDate()) ? "#84C1FF" : (((mm == 12 ? yy + 1 : yy) == new Date().getFullYear() && (mm == 12 ? 1 : mm + 1) == new Date().getMonth() + 1 && meizzWDay[i] == new Date().getDate()) ? "#5CEFA0" : "#f5f5f5"); //将选中的日期显示为凹下去 if ((mm == 12 ? yy + 1 : yy) == outDate.getFullYear() && (mm == 12 ? 1 : mm + 1) == outDate.getMonth() + 1 && meizzWDay[i] == outDate.getDate()) { da.borderColorLight = "#FFFFFF"; da.borderColorDark = "#63A3E9"; } } } else //本月的部分 { da.innerHTML = "<b>" + meizzWDay[i] + "</b>"; da.title = mm + "月" + meizzWDay[i] + "日"; da.onclick = Function("meizzDayClick(this.innerText,0)"); //给td赋予onclick事件的处理 //如果是当前选择的日期,则显示亮蓝色的背景;如果是当前日期,则显示暗黄色背景 if (!outDate) da.style.backgroundColor = (yy == new Date().getFullYear() && mm == new Date().getMonth() + 1 && meizzWDay[i] == new Date().getDate()) ? "#5CEFA0" : "#f5f5f5"; else { da.style.backgroundColor = (yy == outDate.getFullYear() && mm == outDate.getMonth() + 1 && meizzWDay[i] == outDate.getDate()) ? "#84C1FF" : ((yy == new Date().getFullYear() && mm == new Date().getMonth() + 1 && meizzWDay[i] == new Date().getDate()) ? "#5CEFA0" : "#f5f5f5"); //将选中的日期显示为凹下去 if (yy == outDate.getFullYear() && mm == outDate.getMonth() + 1 && meizzWDay[i] == outDate.getDate()) { da.borderColorLight = "#FFFFFF"; da.borderColorDark = "#63A3E9"; } } } da.style.cursor = "hand" } else { da.innerHTML = ""; da.style.backgroundColor = ""; da.style.cursor = "default"; } } } function meizzDayClick(n, ex) //点击显示选取日期,主输入函数************* { parent.meizzTheDate = n; var yy = meizzTheYear; var mm = parseInt(meizzTheMonth) + ex; //ex表示偏移量,用于选择上个月份和下个月份的日期 var hh = meizzTheHour; var mi = meizzTheMinute; var se = meizzTheSecond; //判断月份,并进行对应的处理 if (mm < 1) { yy--; mm = 12 + mm; } else if (mm > 12) { yy++; mm = mm - 12; } if (mm < 10) { mm = "0" + mm; } if (hh < 10) { hh = "0" + hh; } //时 // if (mi<10) {mi="0" + mi;} //分 if (mi < 10) { mi = "" + mi; } //分 if (se < 10) { se = "0" + se; } //秒 if (outObject) { if (!n) { //outObject.value=""; return; } if (n < 10) { n = "0" + n; } WriteDateTo(yy, mm, n, hh, mi, se); closeLayer(); if (bUseTime) { try { outButton.click(); } catch (e) { setday(outObject); } } } else { closeLayer(); alert("您所要输出的控件对象并不存在!"); } } function format(n) //格式化数字为两位字符表示 { var m = new String(); var tmp = new String(n); if (n < 10 && tmp.length < 2) { m = "0" + n; } else { m = n; } return m; } function evaSetTime() //设置用户选择的小时、分钟 { odatelayer.meizzHourHead.innerText = meizzTheHour + " 时"; odatelayer.meizzMinuteHead.innerText = meizzTheMinute + " 分"; odatelayer.meizzSecondHead.innerText = meizzTheSecond + " 秒"; WriteDateTo(meizzTheYear, meizzTheMonth, meizzTheDate, meizzTheHour, meizzTheMinute, meizzTheSecond) } function evaSetTimeNothing() //设置时间控件为空 { odatelayer.meizzHourHead.innerText = ""; odatelayer.meizzMinuteHead.innerText = ""; odatelayer.meizzSecondHead.innerText = ""; WriteDateTo(meizzTheYear, meizzTheMonth, meizzTheDate, meizzTheHour, meizzTheMinute, meizzTheSecond) } function evaSetTimeNow() //设置时间控件为当前时间 { odatelayer.meizzHourHead.innerText = new Date().getHours() + " 时"; odatelayer.meizzMinuteHead.innerText = new Date().getMinutes() + " 分"; odatelayer.meizzSecondHead.innerText = new Date().getSeconds() + " 秒"; meizzTheHour = new Date().getHours(); meizzTheMinute = new Date().getMinutes(); meizzTheSecond = new Date().getSeconds(); WriteDateTo(meizzTheYear, meizzTheMonth, meizzTheDate, meizzTheHour, meizzTheMinute, meizzTheSecond) } function UseTime(ctl) { bUseTime = !bUseTime; if (bUseTime) { bImgSwitch(); ctl.innerHTML = bImg; evaSetTime(); //显示时间,用户原来选择的时间 //evaSetTimeNow(); //显示当前时间 } else { bImgSwitch(); ctl.innerHTML = bImg; evaSetTimeNothing(); } } function WriteDateTo(yy, mm, n, hh, mi, se) { if (bUseTime) { //outObject.value= yy + "/" + format(mm) + "/" + format(n) + " " + format(hh) + ":" + format(mi) + ":" + format(se); //注:在这里你可以输出改成你想要的格式 outObject.value = yy + "/" + format(mm) + "/" + format(n) + " " + format(hh) + ":" + format(mi); } else { //outObject.value= format(mm) + "/" + format(n)+"/"+yy; outObject.value = yy + "/" + format(mm) + "/" + format(n); //注:在这里你可以输出改成你想要的格式 } } function bImgSwitch() { if (bUseTime) { bImg = "開啟"; } else { bImg = "確定"; } } 这是日期控件的内容
09-11
课程设计与要求: 实验18 手写数字识别程序设计与实现 实验类型:设计性实验 实验学时:8 涉及的知识点:SVM、决策树、随机森林、XGBoost和LightGBM机器学习算法的综合应用 一、 实验目的 1、 了解机器学习算法应用项目设计流程与基本方法。 2、 掌握SVM应用设计与K折交叉验证法获得测试数据。 3、 熟悉两种以上不同类型机器学习算法及应用。 4、 掌握各类机器学习算法的区别、优缺点;会应用网格搜索选择最优超参数。 5、 掌握分类任务的性能指标评价方法。 二、 实验要求 1、 使用anaconda集成开发环境完成课程设计,代码的可维护性好,有必要的注释和相应的文档。 2、 能够识别符合分辨率要求的手写数字。 3、 构建不同模型实现手写数字分类识别,至少要对比两种方法,如决策树、支持向量机、随机森林、XGBoost和LightGBM等。对比不同模型的分类性能报告,评价模型好坏。 4、 数据集采用sklearn.datasets中的digits,测试集数据可以用自己手写产生或者从digits中拆分。 三、 设计指标 1、 完整的设计文档 1) 系统的需求分析 2) 系统的概要设计 3) 详细设计与实现 4) 系统测试方法 2、 运行画面截图 3、 每一部分附上关键性代码 4、 项目总结 四、 预习与参考 1、 教材有关决策树、SVM(支持向量机)、随机森林、XGBoost和LightGBM有关章节。 2、 课程PPT有关内容。 3、 中国知网有关手写数字识别的文献资料。 五、 考核形式 根据提交的设计文档完成程度以及程序功能的实现情况(要求演示)进行考核:  无任何文档,无程序,得 0 分;  文档描述不清楚,思路混乱,程序不能运行,2分;  文档描述清晰,程序实现了基本功能,3.5分;  文档描述清晰准确,思路清晰,程序实现了要求的所有功能,4. 5分;  文档完备,设计合理有创新,报告清晰明确,深入分析了自己进行实验的体会感想,程序实现了全部功能,功能完善,并有其它的创新实现,5分。 六、 实验报告要求 1、 实验目的结合自己个人的实际情况书写,不要雷同。 2、 项目概要设计说明书(描述软件系统架构、逻辑架构、物理架构、部署结构、功能架构及关键技术,关键业务模块需通过UML图进行详细描述)、需求规格说明书(包括功能设计、非功能性设计、系统用例)。 3、 项目设计运行截图。 代码程序: # ======================== # 导入必要的库 # ======================== import numpy as np # 数计算库 import matplotlib.pyplot as plt # 绘图库 import pandas as pd # 数据处理库 import tkinter as tk # GUI库 from tkinter import ttk, filedialog, messagebox # GUI组件 from PIL import Image, ImageDraw # 图像处理库 import cv2 # 计算机视觉库 import os # 操作系统接口 import csv # CSV文件处理 from sklearn.datasets import load_digits # 加载数字数据集 from sklearn.model_selection import train_test_split # 数据集划分 from sklearn.svm import SVC # 支持向量机模型 from sklearn.tree import DecisionTreeClassifier # 决策树模型 from sklearn.ensemble import RandomForestClassifier # 随机森林模型 from sklearn.neural_network import MLPClassifier # 多层感知机模型 from sklearn.neighbors import KNeighborsClassifier # K近邻模型 from sklearn.naive_bayes import GaussianNB # 朴素贝叶斯模型 from sklearn.metrics import accuracy_score # 准确率评估 from sklearn.preprocessing import StandardScaler # 数据标准化 # 设置中文字体和负号显示(解决中文乱码问题) plt.rcParams["font.family"] = ["SimHei", "Microsoft YaHei"] plt.rcParams["axes.unicode_minus"] = False # ======================== # 尝试导入可选模型库 # ======================== XGB_INSTALLED = False # 标记XGBoost是否安装 LGB_INSTALLED = False # 标记LightGBM是否安装 try: import xgboost as xgb # XGBoost模型 XGB_INSTALLED = True except ImportError: print("警告: 未安装XGBoost库,无法使用XGBoost模型") try: import lightgbm as lgb # LightGBM模型 LGB_INSTALLED = True except ImportError: print("警告: 未安装LightGBM库,无法使用LightGBM模型") # ======================== # 模型配置 # ======================== # 定义模型元数据(包含模型名称、类、标准化器和参数) MODEL_METADATA = { 'svm': ('支持向量机(SVM)', SVC, StandardScaler, {'probability': True, 'random_state': 42}), 'dt': ('决策树(DT)', DecisionTreeClassifier, None, {'random_state': 42}), 'rf': ('随机森林(RF)', RandomForestClassifier, None, {'n_estimators': 100, 'random_state': 42}), 'mlp': ('多层感知机(MLP)', MLPClassifier, StandardScaler, {'hidden_layer_sizes': (100, 50), 'max_iter': 500, 'random_state': 42}), 'knn': ('K最近邻(KNN)', KNeighborsClassifier, StandardScaler, {'n_neighbors': 5, 'weights': 'distance'}), 'nb': ('高斯朴素贝叶斯(NB)', GaussianNB, None, {}), } # 添加可选模型(如果已安装) if XGB_INSTALLED: MODEL_METADATA['xgb'] = ('XGBoost(XGB)', xgb.XGBClassifier, None, {'objective': 'multi:softmax', 'random_state': 42}) if LGB_INSTALLED: MODEL_METADATA['lgb'] = ('LightGBM(LGB)', lgb.LGBMClassifier, None, { 'objective': 'multiclass', 'random_state': 42, 'num_class': 10, 'max_depth': 5, 'min_child_samples': 10, 'learning_rate': 0.1, 'force_col_wise': True }) # ======================== # 模型工厂类 - 负责模型创建、训练和评估 # ======================== class ModelFactory: @staticmethod def get_split_data(digits_dataset): """数据集划分""" X, y = digits_dataset.data, digits_dataset.target # 获取特征和标签 # 划分训练集和测试集(70%训练,30%测试) return train_test_split(X, y, test_size=0.3, random_state=42) @classmethod def create_model(cls, model_type): """创建模型和数据标准化器""" # 检查模型类型是否有效 if model_type not in MODEL_METADATA: raise ValueError(f"未知模型类型: {model_type}") # 从配置中获取模型信息 name, model_cls, scaler_cls, params = MODEL_METADATA[model_type] # 创建模型实例和标准化器 model = model_cls(**params) scaler = scaler_cls() if scaler_cls else None return model, scaler @staticmethod def train_model(model, X_train, y_train, scaler=None, model_type=None): """训练模型""" # 数据标准化处理 if scaler: X_train = scaler.fit_transform(X_train) # LightGBM特殊处理(需要DataFrame格式) if model_type == 'lgb' and isinstance(X_train, np.ndarray): X_train = pd.DataFrame(X_train) # 训练模型 model.fit(X_train, y_train) return model @staticmethod def evaluate_model(model, X_test, y_test, scaler=None, model_type=None): """评估模型""" # 数据标准化处理 if scaler: X_test = scaler.transform(X_test) # LightGBM特殊处理 if model_type == 'lgb' and isinstance(X_test, np.ndarray) and hasattr(model, 'feature_name_'): X_test = pd.DataFrame(X_test, columns=model.feature_name_) # 预测并计算准确率 y_pred = model.predict(X_test) return accuracy_score(y_test, y_pred) @classmethod def train_and_evaluate(cls, model_type, X_train, y_train, X_test, y_test): """训练并评估模型""" try: # 创建模型 model, scaler = cls.create_model(model_type) # 训练模型 model = cls.train_model(model, X_train, y_train, scaler, model_type) # 评估模型 accuracy = cls.evaluate_model(model, X_test, y_test, scaler, model_type) return model, scaler, accuracy except Exception as e: print(f"模型 {model_type} 训练/评估错误: {str(e)}") raise @classmethod def evaluate_all_models(cls, digits_dataset): """评估所有可用模型""" print("\n=== 模型评估 ===") # 划分数据集 X_train, X_test, y_train, y_test = cls.get_split_data(digits_dataset) results = [] # 存储结果 # 遍历所有模型 for model_type in MODEL_METADATA: name = MODEL_METADATA[model_type][0] print(f"评估模型: {name} ({model_type})") # 检查模型是否可用 if not MODEL_METADATA[model_type][1]: results.append({"模型名称": name, "准确率": "N/A"}) continue try: # 训练并评估模型 _, _, accuracy = cls.train_and_evaluate( model_type, X_train, y_train, X_test, y_test ) results.append({"模型名称": name, "准确率": f"{accuracy:.4f}"}) except Exception as e: results.append({"模型名称": name, "准确率": f"错误: {str(e)}"}) # 按准确率排序 results.sort( key=lambda x: float(x["准确率"]) if isinstance(x["准确率"], str) and x["准确率"].replace('.', '', 1).isdigit() else -1, reverse=True ) # 打印结果 print(pd.DataFrame(results)) return results # ======================== # 手写板类 - GUI界面和绘图功能 # ======================== class HandwritingBoard: CANVAS_SIZE = 300 # 固定画布尺寸 BRUSH_SIZE = 12 # 画笔大小 def __init__(self, root, model_factory, digits): # 初始化主窗口 self.root = root self.root.title("手写数字识别系统") self.root.geometry("1000x700") # 设置窗口大小 # 模型和数据相关 self.model_factory = model_factory # 模型工厂 self.digits = digits # 数字数据集 self.model_cache = {} # 模型缓存(提高切换速度) self.current_model = None # 当前使用的模型 self.scaler = None # 数据标准化器 self.current_model_type = None # 当前模型类型 self.has_drawn = False # 标记是否已绘制数字 self.custom_data = [] # 存储自定义训练数据 # 绘图相关状态 self.drawing = False # 是否正在绘制 self.last_x = self.last_y = 0 # 上次绘制位置 # 创建自定义数据目录 self.data_dir = "custom_digits_data" os.makedirs(self.data_dir, exist_ok=True) # 初始化画布(PIL图像) self.image = Image.new("L", (self.CANVAS_SIZE, self.CANVAS_SIZE), 255) # 创建白色背景图像 self.draw_obj = ImageDraw.Draw(self.image) # 创建绘图对象 # 创建界面组件 self.create_widgets() # 初始化默认模型 self.init_default_model() def create_widgets(self): """创建界面组件""" # 创建主架 main_frame = tk.Frame(self.root) main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 1. 模型选择区域 model_frame = tk.LabelFrame(main_frame, text="模型选择", font=("Arial", 10, "bold")) model_frame.grid(row=0, column=0, columnspan=2, sticky="ew", padx=5, pady=5) model_frame.grid_columnconfigure(1, weight=1) # 让模型标签可以扩展 # 模型选择标签 tk.Label(model_frame, text="选择模型:", font=("Arial", 10)).grid(row=0, column=0, padx=5, pady=5, sticky="w") # 获取可用模型列表 self.available_models = [] for model_type, (name, _, _, _) in MODEL_METADATA.items(): if MODEL_METADATA[model_type][1]: self.available_models.append((model_type, name)) # 模型选择下拉 self.model_var = tk.StringVar() self.model_combobox = ttk.Combobox( model_frame, textvariable=self.model_var, values=[name for _, name in self.available_models], state="readonly", width=25, font=("Arial", 10) ) self.model_combobox.current(0) # 设置默认选项 self.model_combobox.bind("<<ComboboxSelected>>", self.on_model_select) # 绑定选择事件 self.model_combobox.grid(row=0, column=1, padx=5, pady=5, sticky="ew") # 模型信息标签(显示准确率) self.model_label = tk.Label( model_frame, text="", font=("Arial", 10), relief=tk.SUNKEN, padx=5, pady=2 ) self.model_label.grid(row=0, column=2, padx=5, pady=5, sticky="ew") # 2. 左侧绘图区域和右侧结果区域 # 左侧绘图区域 left_frame = tk.LabelFrame(main_frame, text="绘制区域", font=("Arial", 10, "bold")) left_frame.grid(row=1, column=0, padx=5, pady=5, sticky="nsew") # 绘图画布 self.canvas = tk.Canvas(left_frame, bg="white", width=self.CANVAS_SIZE, height=self.CANVAS_SIZE) self.canvas.pack(padx=10, pady=10) # 绑定绘图事件 self.canvas.bind("<Button-1>", self.start_draw) # 鼠标按下 self.canvas.bind("<B1-Motion>", self.draw) # 鼠标拖动 self.canvas.bind("<ButtonRelease-1>", self.stop_draw) # 鼠标释放 # 添加绘制提示 self.canvas.create_text( self.CANVAS_SIZE / 2, self.CANVAS_SIZE / 2, text="绘制数字", fill="gray", font=("Arial", 16) ) # 绘图控制按钮 btn_frame = tk.Frame(left_frame) btn_frame.pack(fill=tk.X, pady=(0, 10)) # 功能按钮 tk.Button(btn_frame, text="识别", command=self.recognize, width=8).pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="清除", command=self.clear_canvas, width=8).pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="样本", command=self.show_samples, width=8).pack(side=tk.LEFT, padx=5) # 右侧结果区域 right_frame = tk.Frame(main_frame) right_frame.grid(row=1, column=1, padx=5, pady=5, sticky="nsew") # 2.1 识别结果区域 result_frame = tk.LabelFrame(right_frame, text="识别结果", font=("Arial", 10, "bold")) result_frame.pack(fill=tk.X, padx=5, pady=5) # 结果显示标签 self.result_label = tk.Label( result_frame, text="请绘制数字", font=("Arial", 24), pady=10 ) self.result_label.pack() # 置信度显示标签 self.prob_label = tk.Label( result_frame, text="", font=("Arial", 12) ) self.prob_label.pack() # 2.2 置信度可视化区域 confidence_frame = tk.LabelFrame(right_frame, text="识别置信度", font=("Arial", 10, "bold")) confidence_frame.pack(fill=tk.X, padx=5, pady=5) # 置信度画布(条形图) self.confidence_canvas = tk.Canvas( confidence_frame, bg="white", height=50 ) self.confidence_canvas.pack(fill=tk.X, padx=10, pady=10) self.confidence_canvas.create_text( 150, 25, text="识别后显示置信度", fill="gray", font=("Arial", 10) ) # 2.3 候选数字区域 candidates_frame = tk.LabelFrame(right_frame, text="可能的数字", font=("Arial", 10, "bold")) candidates_frame.pack(fill=tk.X, padx=5, pady=5) # 候选数字表格 columns = ("数字", "概率") self.candidates_tree = ttk.Treeview( candidates_frame, columns=columns, show="headings", height=4 ) # 配置表格列 for col in columns: self.candidates_tree.heading(col, text=col) self.candidates_tree.column(col, width=80, anchor=tk.CENTER) # 添加滚动条 scrollbar = ttk.Scrollbar( candidates_frame, orient=tk.VERTICAL, command=self.candidates_tree.yview ) self.candidates_tree.configure(yscroll=scrollbar.set) self.candidates_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5) scrollbar.pack(side=tk.RIGHT, fill=tk.Y, padx=5, pady=5) # 3. 模型性能对比和训练集管理区域 # 3.1 模型性能对比区域 performance_frame = tk.LabelFrame(main_frame, text="模型性能对比", font=("Arial", 10, "bold")) performance_frame.grid(row=2, column=0, padx=5, pady=5, sticky="nsew") # 性能表格 columns = ("模型名称", "准确率") self.performance_tree = ttk.Treeview( performance_frame, columns=columns, show="headings", height=8 ) # 配置表格列 for col in columns: self.performance_tree.heading(col, text=col) self.performance_tree.column(col, width=120, anchor=tk.CENTER) # 添加滚动条 scrollbar = ttk.Scrollbar( performance_frame, orient=tk.VERTICAL, command=self.performance_tree.yview ) self.performance_tree.configure(yscroll=scrollbar.set) self.performance_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5) scrollbar.pack(side=tk.RIGHT, fill=tk.Y, padx=5, pady=5) # 3.2 训练集管理区域 train_frame = tk.LabelFrame(main_frame, text="训练集管理", font=("Arial", 10, "bold")) train_frame.grid(row=2, column=1, padx=5, pady=5, sticky="nsew") # 训练集管理按钮 tk.Button( train_frame, text="保存为训练样本", command=self.save_as_training_sample, width=18, height=2 ).grid(row=0, column=0, padx=5, pady=5, sticky="ew") tk.Button( train_frame, text="保存全部训练集", command=self.save_all_training_data, width=18, height=2 ).grid(row=0, column=1, padx=5, pady=5, sticky="ew") tk.Button( train_frame, text="加载训练集", command=self.load_training_data, width=18, height=2 ).grid(row=1, column=0, padx=5, pady=5, sticky="ew") tk.Button( train_frame, text="性能图表", command=self.show_performance_chart, width=18, height=2 ).grid(row=1, column=1, padx=5, pady=5, sticky="ew") # 4. 状态栏 self.status_var = tk.StringVar(value="就绪") status_bar = tk.Label( self.root, textvariable=self.status_var, bd=1, relief=tk.SUNKEN, anchor=tk.W, font=("Arial", 10) ) status_bar.pack(side=tk.BOTTOM, fill=tk.X) # 配置布局权重 main_frame.grid_columnconfigure(0, weight=1) main_frame.grid_columnconfigure(1, weight=1) main_frame.grid_rowconfigure(1, weight=1) main_frame.grid_rowconfigure(2, weight=1) # ======================== # 绘图功能 # ======================== def start_draw(self, event): """开始绘制""" self.drawing = True self.last_x, self.last_y = event.x, event.y def draw(self, event): """绘制""" if not self.drawing: return x, y = event.x, event.y # 在画布上绘制 self.canvas.create_line( self.last_x, self.last_y, x, y, fill="black", width=self.BRUSH_SIZE, capstyle=tk.ROUND, smooth=True ) # 在图像上绘制(用于后续处理) self.draw_obj.line( [self.last_x, self.last_y, x, y], fill=0, # 黑色 width=self.BRUSH_SIZE ) # 更新位置 self.last_x, self.last_y = x, y def stop_draw(self, event): """停止绘制""" self.drawing = False self.has_drawn = True self.status_var.set("已绘制数字,点击'识别'进行识别") def clear_canvas(self): """清除画布""" # 清除画布内容 self.canvas.delete("all") # 重置图像 self.image = Image.new("L", (self.CANVAS_SIZE, self.CANVAS_SIZE), 255) # 白色背景 self.draw_obj = ImageDraw.Draw(self.image) # 添加绘制提示 self.canvas.create_text( self.CANVAS_SIZE / 2, self.CANVAS_SIZE / 2, text="绘制数字", fill="gray", font=("Arial", 16) ) # 重置结果显示 self.result_label.config(text="请绘制数字") self.prob_label.config(text="") self.clear_confidence_display() self.has_drawn = False self.status_var.set("画布已清除") def clear_confidence_display(self): """清除置信度显示""" self.confidence_canvas.delete("all") self.confidence_canvas.create_text( 150, 25, text="识别后显示置信度", fill="gray", font=("Arial", 10) ) # 清空候选数字表格 for item in self.candidates_tree.get_children(): self.candidates_tree.delete(item) # ======================== # 图像处理功能 # ======================== def preprocess_image(self): """预处理手写数字图像""" # 将PIL图像转换为NumPy数组 img_array = np.array(self.image) # 1. 高斯模糊降噪 img_array = cv2.GaussianBlur(img_array, (5, 5), 0) # 2. 二化(转换为黑白图像) _, img_array = cv2.threshold(img_array, 127, 255, cv2.THRESH_BINARY_INV) # 3. 轮廓检测(查找数字轮廓) contours, _ = cv2.findContours(img_array, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: self.status_var.set("未检测到有效数字,请重新绘制") return None # 4. 找到最大轮廓(即数字部分) c = max(contours, key=cv2.contourArea) x, y, w, h = cv2.boundingRect(c) # 5. 提取数字区域 digit = img_array[y:y+h, x:x+w] # 6. 填充为正方形(保持长宽比) size = max(w, h) padded = np.ones((size, size), dtype=np.uint8) * 255 # 白色背景 offset_x = (size - w) // 2 offset_y = (size - h) // 2 padded[offset_y:offset_y+h, offset_x:offset_x+w] = digit # 7. 缩放为8x8(匹配MNIST数据集格式) resized = cv2.resize(padded, (8, 8), interpolation=cv2.INTER_AREA) # 8. 归一化(将像素从0-255映射到0-16) normalized = 16 - (resized / 255 * 16).astype(np.uint8) # 9. 展平为一维数组(64个特征) return normalized.flatten() # ======================== # 识别功能 # ======================== def recognize(self): """识别手写数字""" # 检查是否已绘制数字 if not self.has_drawn: self.status_var.set("请先绘制数字再识别") return # 检查模型是否已加载 if self.current_model is None: self.status_var.set("模型未加载,请选择模型") return # 预处理图像 img_array = self.preprocess_image() if img_array is None: return # 重塑为模型输入格式(1个样本,64个特征) img_input = img_array.reshape(1, -1) try: # 数据标准化 if self.scaler: img_input = self.scaler.transform(img_input) # LightGBM特殊处理(需要DataFrame格式) if self.current_model_type == 'lgb' and hasattr(self.current_model, 'feature_name_'): img_input = pd.DataFrame(img_input, columns=self.current_model.feature_name_) # 预测数字 pred = self.current_model.predict(img_input)[0] self.result_label.config(text=f"识别结果: {pred}") # 概率预测(如果模型支持) if hasattr(self.current_model, 'predict_proba'): probs = self.current_model.predict_proba(img_input)[0] confidence = probs[pred] # 预测结果的置信度 # 更新UI self.prob_label.config(text=f"置信度: {confidence:.2%}") self.update_confidence_display(confidence) # 更新置信度可视化 # 显示候选数字(概率最高的3个) top3 = sorted(enumerate(probs), key=lambda x: -x[1])[:3] self.update_candidates_display(top3) else: self.prob_label.config(text="该模型不支持概率输出") self.clear_confidence_display() self.status_var.set(f"识别完成: 数字 {pred}") except Exception as e: self.status_var.set(f"识别错误: {str(e)}") self.clear_confidence_display() # ======================== # UI更新功能 # ======================== def update_confidence_display(self, confidence): """更新置信度可视化""" self.confidence_canvas.delete("all") # 获取画布宽度 canvas_width = self.confidence_canvas.winfo_width() or 300 # 绘制背景 self.confidence_canvas.create_rectangle( 10, 10, canvas_width - 10, 40, fill="#f0f0f0", outline="#cccccc" ) # 绘制置信度条(根据置信度) bar_width = int((canvas_width - 20) * confidence) color = self.get_confidence_color(confidence) # 根据置信度选择颜色 self.confidence_canvas.create_rectangle( 10, 10, 10 + bar_width, 40, fill=color, outline="" ) # 绘制文本(显示百分比) self.confidence_canvas.create_text( canvas_width / 2, 25, text=f"{confidence:.1%}", font=("Arial", 10, "bold") ) # 绘制刻度 for i in range(0, 11): x_pos = 10 + i * (canvas_width - 20) / 10 self.confidence_canvas.create_line(x_pos, 40, x_pos, 45, width=1) if i % 2 == 0: self.confidence_canvas.create_text(x_pos, 55, text=f"{i*10}%", font=("Arial", 8)) def get_confidence_color(self, confidence): """根据置信度获取颜色""" # 高置信度:绿色 if confidence >= 0.9: return "#4CAF50" # 中等置信度:黄色 elif confidence >= 0.7: return "#FFC107" # 低置信度:红色 else: return "#F44336" def update_candidates_display(self, candidates): """更新候选数字显示""" # 清空现有项 for item in self.candidates_tree.get_children(): self.candidates_tree.delete(item) # 添加新项(候选数字及其概率) for digit, prob in candidates: self.candidates_tree.insert( "", tk.END, values=(digit, f"{prob:.2%}") ) # ======================== # 样本显示功能 # ======================== def show_samples(self): """显示样本图像""" plt.figure(figsize=(10, 4)) # 显示0-9每个数字的一个样本 for i in range(10): plt.subplot(2, 5, i+1) sample_idx = np.where(self.digits.target == i)[0][0] plt.imshow(self.digits.images[sample_idx], cmap="gray") plt.title(f"数字 {i}", fontsize=9) plt.axis("off") plt.tight_layout() plt.show() # ======================== # 模型管理功能 # ======================== def on_model_select(self, event): """模型选择事件处理""" # 获取选中的模型名称 selected_name = self.model_var.get() # 查找对应的模型类型 model_type = next( (k for k, v in self.available_models if v == selected_name), None ) if model_type: # 切换模型 self.change_model(model_type) def change_model(self, model_type): """切换模型""" model_name = MODEL_METADATA[model_type][0] # 尝试从缓存加载模型 if model_type in self.model_cache: self.current_model, self.scaler, accuracy, self.current_model_type = self.model_cache[model_type] self.model_label.config(text=f"{model_name} (准确率:{accuracy:.4f})") self.status_var.set(f"已加载模型: {model_name}") return # 加载新模型 self.status_var.set(f"正在加载模型: {model_name}...") self.root.update() # 更新UI显示状态 try: # 获取数据集 X_train, X_test, y_train, y_test = self.model_factory.get_split_data(self.digits) # 训练并评估模型 self.current_model, self.scaler, accuracy = self.model_factory.train_and_evaluate( model_type, X_train, y_train, X_test, y_test ) # 缓存模型 self.current_model_type = model_type self.model_cache[model_type] = (self.current_model, self.scaler, accuracy, self.current_model_type) # 更新UI self.model_label.config(text=f"{model_name} (准确率:{accuracy:.4f})") self.status_var.set(f"模型加载完成: {model_name}, 准确率: {accuracy:.4f}") self.clear_canvas() # 更新性能表格 self.load_performance_data() except Exception as e: self.status_var.set(f"模型加载失败: {str(e)}") self.model_label.config(text="模型加载失败") def init_default_model(self): """初始化默认模型""" # 设置默认模型并加载 self.model_var.set(self.available_models[0][1]) self.change_model(self.available_models[0][0]) def load_performance_data(self): """加载性能数据""" # 评估所有模型 results = self.model_factory.evaluate_all_models(self.digits) # 清空表格 for item in self.performance_tree.get_children(): self.performance_tree.delete(item) # 添加数据到表格 for i, result in enumerate(results): tag = "highlight" if i == 0 else "" # 高亮显示性能最好的模型 self.performance_tree.insert( "", tk.END, values=(result["模型名称"], result["准确率"]), tags=(tag,) ) # 配置高亮样式 self.performance_tree.tag_configure("highlight", background="#e6f7ff") # ======================== # 性能可视化功能 # ======================== def show_performance_chart(self): """显示性能图表""" # 获取性能数据 results = self.model_factory.evaluate_all_models(self.digits) # 提取有效结果(过滤掉错误数据) valid_results = [] for result in results: try: accuracy = float(result["准确率"]) valid_results.append((result["模型名称"], accuracy)) except ValueError: continue if not valid_results: messagebox.showinfo("提示", "没有可用的性能数据") return # 按准确率排序 valid_results.sort(key=lambda x: x[1], reverse=True) models, accuracies = zip(*valid_results) # 创建水平条形图 plt.figure(figsize=(10, 5)) bars = plt.barh(models, accuracies, color='#2196F3') plt.xlabel('准确率', fontsize=10) plt.ylabel('模型', fontsize=10) plt.title('模型性能对比', fontsize=12) plt.xlim(0, 1.05) # 设置X轴范围 # 添加数标签 for bar in bars: width = bar.get_width() plt.text( width + 0.01, bar.get_y() + bar.get_height()/2, f'{width:.4f}', ha='left', va='center', fontsize=8 ) plt.tight_layout() plt.show() # ======================== # 训练集管理功能 # ======================== def save_as_training_sample(self): """保存为训练样本""" # 检查是否已绘制数字 if not self.has_drawn: self.status_var.set("请先绘制数字再保存") return # 预处理图像 img_array = self.preprocess_image() if img_array is None: return # 弹出标签输入窗口 label_window = tk.Toplevel(self.root) label_window.title("输入标签") label_window.geometry("300x150") label_window.transient(self.root) label_window.grab_set() # 模态窗口 # 标签输入提示 tk.Label( label_window, text="请输入数字标签 (0-9):", font=("Arial", 10) ).pack(pady=10) # 输入框 entry = tk.Entry(label_window, font=("Arial", 12), width=5) entry.pack(pady=5) entry.focus_set() def save_with_label(): """保存带标签的样本""" try: # 验证标签 label = int(entry.get()) if label < 0 or label > 9: raise ValueError("标签必须是0-9的数字") # 添加到自定义数据集 self.custom_data.append((img_array.tolist(), label)) self.status_var.set(f"已保存数字 {label} (共 {len(self.custom_data)} 个样本)") label_window.destroy() except ValueError as e: self.status_var.set(f"保存错误: {str(e)}") # 保存按钮 tk.Button( label_window, text="保存", command=save_with_label, width=10 ).pack(pady=5) def save_all_training_data(self): """保存全部训练数据""" # 检查是否有数据可保存 if not self.custom_data: self.status_var.set("没有训练数据可保存") return # 弹出文件保存对话 file_path = filedialog.asksaveasfilename( defaultextension=".csv", filetypes=[("CSV文件", "*.csv")], initialfile="custom_digits.csv", title="保存训练集" ) if not file_path: return try: # 写入CSV文件 with open(file_path, 'w', newline='', encoding='utf-8') as f: writer = csv.writer(f) # 写入表头(64个像素+标签) writer.writerow([f'pixel{i}' for i in range(64)] + ['label']) # 写入数据 for img_data, label in self.custom_data: writer.writerow(img_data + [label]) self.status_var.set(f"已保存 {len(self.custom_data)} 个样本到 {os.path.basename(file_path)}") except Exception as e: self.status_var.set(f"保存失败: {str(e)}") def load_training_data(self): """加载训练数据""" # 弹出文件选择对话 file_path = filedialog.askopenfilename( filetypes=[("CSV文件", "*.csv")], title="加载训练集" ) if not file_path: return try: self.custom_data = [] # 读取CSV文件 with open(file_path, 'r', newline='', encoding='utf-8') as f: reader = csv.reader(f) next(reader) # 跳过标题行 # 解析每一行数据 for row in reader: if len(row) != 65: # 64像素+1标签 continue # 提取像素数据和标签 img_data = [float(pixel) for pixel in row[:64]] label = int(row[64]) self.custom_data.append((img_data, label)) self.status_var.set(f"已加载 {len(self.custom_data)} 个样本") except Exception as e: self.status_var.set(f"加载失败: {str(e)}") # ======================== # 主程序入口 # ======================== def run(self): """运行应用""" self.root.mainloop() # ======================== # 程序入口 # ======================== if __name__ == "__main__": digits = load_digits() # 加载数字数据集 root = tk.Tk() # 创建主窗口 app = HandwritingBoard(root, ModelFactory, digits) # 创建应用实例 app.run() # 运行应用 请你根据上面的内容生成符合要求的课程设计:
06-24
import numpy as np import matplotlib.pyplot as plt import pandas as pd import tkinter as tk from tkinter import ttk, filedialog, messagebox from PIL import Image, ImageDraw import cv2 import os import csv from sklearn.datasets import load_digits from sklearn.model_selection import train_test_split from sklearn.svm import SVC from sklearn.tree import DecisionTreeClassifier from sklearn.ensemble import RandomForestClassifier from sklearn.neural_network import MLPClassifier from sklearn.neighbors import KNeighborsClassifier from sklearn.naive_bayes import GaussianNB from sklearn.metrics import accuracy_score from sklearn.preprocessing import StandardScaler # 设置中文字体和负号显示 plt.rcParams["font.family"] = ["SimHei", "Microsoft YaHei"] plt.rcParams["axes.unicode_minus"] = False # 尝试导入XGBoost和LightGBM XGB_INSTALLED = False LGB_INSTALLED = False try: import xgboost as xgb XGB_INSTALLED = True except ImportError: print("警告: 未安装XGBoost库,无法使用XGBoost模型") try: import lightgbm as lgb LGB_INSTALLED = True except ImportError: print("警告: 未安装LightGBM库,无法使用LightGBM模型") # 定义模型元数据常量(优化参数) MODEL_METADATA = { 'svm': ('支持向量机(SVM)', SVC, StandardScaler, {'probability': True, 'random_state': 42}), 'dt': ('决策树(DT)', DecisionTreeClassifier, None, {'random_state': 42}), 'rf': ('随机森林(RF)', RandomForestClassifier, None, {'n_estimators': 100, 'random_state': 42}), 'mlp': ('多层感知机(MLP)', MLPClassifier, StandardScaler, {'hidden_layer_sizes': (100, 50), 'max_iter': 500, 'random_state': 42}), 'knn': ('K最近邻(KNN)', KNeighborsClassifier, StandardScaler, {'n_neighbors': 5, 'weights': 'distance'}), 'nb': ('高斯朴素贝叶斯(NB)', GaussianNB, None, {}), } # 添加可选模型 if XGB_INSTALLED: MODEL_METADATA['xgb'] = ('XGBoost(XGB)', xgb.XGBClassifier, None, {'objective': 'multi:softmax', 'random_state': 42}) if LGB_INSTALLED: MODEL_METADATA['lgb'] = ('LightGBM(LGB)', lgb.LGBMClassifier, None, { 'objective': 'multiclass', 'random_state': 42, 'num_class': 10, 'max_depth': 5, 'min_child_samples': 10, 'learning_rate': 0.1, 'force_col_wise': True }) class ModelFactory: @staticmethod def get_split_data(digits_dataset): """数据集划分""" X, y = digits_dataset.data, digits_dataset.target return train_test_split(X, y, test_size=0.3, random_state=42) @classmethod def create_model(cls, model_type): """创建模型和数据标准化器""" if model_type not in MODEL_METADATA: raise ValueError(f"未知模型类型: {model_type}") name, model_cls, scaler_cls, params = MODEL_METADATA[model_type] if not model_cls: raise ImportError(f"{name}模型依赖库未安装") model = model_cls(**params) scaler = scaler_cls() if scaler_cls else None return model, scaler @staticmethod def train_model(model, X_train, y_train, scaler=None, model_type=None): """训练模型""" if scaler: X_train = scaler.fit_transform(X_train) if model_type == 'lgb' and isinstance(X_train, np.ndarray): X_train = pd.DataFrame(X_train) model.fit(X_train, y_train) return model @staticmethod def evaluate_model(model, X_test, y_test, scaler=None, model_type=None): """评估模型""" if scaler: X_test = scaler.transform(X_test) if model_type == 'lgb' and isinstance(X_test, np.ndarray) and hasattr(model, 'feature_name_'): X_test = pd.DataFrame(X_test, columns=model.feature_name_) y_pred = model.predict(X_test) return accuracy_score(y_test, y_pred) @classmethod def train_and_evaluate(cls, model_type, X_train, y_train, X_test, y_test): """训练并评估模型""" try: model, scaler = cls.create_model(model_type) model = cls.train_model(model, X_train, y_train, scaler, model_type) accuracy = cls.evaluate_model(model, X_test, y_test, scaler, model_type) return model, scaler, accuracy except Exception as e: print(f"模型 {model_type} 训练/评估错误: {str(e)}") raise @classmethod def evaluate_all_models(cls, digits_dataset): """评估所有可用模型""" print("\n=== 模型评估 ===") X_train, X_test, y_train, y_test = cls.get_split_data(digits_dataset) results = [] for model_type in MODEL_METADATA: name = MODEL_METADATA[model_type][0] print(f"评估模型: {name} ({model_type})") if not MODEL_METADATA[model_type][1]: results.append({"模型名称": name, "准确率": "N/A"}) continue try: _, _, accuracy = cls.train_and_evaluate( model_type, X_train, y_train, X_test, y_test ) results.append({"模型名称": name, "准确率": f"{accuracy:.4f}"}) except Exception as e: results.append({"模型名称": name, "准确率": f"错误: {str(e)}"}) # 按准确率排序 results.sort( key=lambda x: float(x["准确率"]) if isinstance(x["准确率"], str) and x["准确率"].replace('.', '', 1).isdigit() else -1, reverse=True ) print(pd.DataFrame(results)) return results class HandwritingBoard: CANVAS_SIZE = 300 # 固定画布尺寸 BRUSH_SIZE = 12 # 画笔大小 def __init__(self, root, model_factory, digits): self.root = root self.root.title("手写数字识别系统") self.root.geometry("1000x700") # 增加窗口尺寸以容纳所有组件 self.model_factory = model_factory self.digits = digits self.model_cache = {} self.current_model = None self.scaler = None self.current_model_type = None self.has_drawn = False self.custom_data = [] self.drawing = False self.last_x = self.last_y = 0 # 自定义数据目录 self.data_dir = "custom_digits_data" os.makedirs(self.data_dir, exist_ok=True) # 初始化画布 self.image = Image.new("L", (self.CANVAS_SIZE, self.CANVAS_SIZE), 255) self.draw_obj = ImageDraw.Draw(self.image) self.create_widgets() self.init_default_model() def create_widgets(self): """使用grid布局管理器创建界面组件""" # 创建主架 main_frame = tk.Frame(self.root) main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 使用grid布局管理器 # 第一行:模型选择区域 model_frame = tk.LabelFrame(main_frame, text="模型选择", font=("Arial", 10, "bold")) model_frame.grid(row=0, column=0, columnspan=2, sticky="ew", padx=5, pady=5) model_frame.grid_columnconfigure(1, weight=1) # 让模型标签可以扩展 tk.Label(model_frame, text="选择模型:", font=("Arial", 10)).grid(row=0, column=0, padx=5, pady=5, sticky="w") self.available_models = [] for model_type, (name, _, _, _) in MODEL_METADATA.items(): if MODEL_METADATA[model_type][1]: self.available_models.append((model_type, name)) self.model_var = tk.StringVar() self.model_combobox = ttk.Combobox( model_frame, textvariable=self.model_var, values=[name for _, name in self.available_models], state="readonly", width=25, font=("Arial", 10) ) self.model_combobox.current(0) self.model_combobox.bind("<<ComboboxSelected>>", self.on_model_select) self.model_combobox.grid(row=0, column=1, padx=5, pady=5, sticky="ew") self.model_label = tk.Label( model_frame, text="", font=("Arial", 10), relief=tk.SUNKEN, padx=5, pady=2 ) self.model_label.grid(row=0, column=2, padx=5, pady=5, sticky="ew") # 第二行:左侧绘图区域和右侧结果区域 # 左侧绘图区域 left_frame = tk.LabelFrame(main_frame, text="绘制区域", font=("Arial", 10, "bold")) left_frame.grid(row=1, column=0, padx=5, pady=5, sticky="nsew") self.canvas = tk.Canvas(left_frame, bg="white", width=self.CANVAS_SIZE, height=self.CANVAS_SIZE) self.canvas.pack(padx=10, pady=10) self.canvas.bind("<Button-1>", self.start_draw) self.canvas.bind("<B1-Motion>", self.draw) self.canvas.bind("<ButtonRelease-1>", self.stop_draw) # 添加绘制提示 self.canvas.create_text( self.CANVAS_SIZE / 2, self.CANVAS_SIZE / 2, text="绘制数字", fill="gray", font=("Arial", 16) ) # 绘图控制按钮 btn_frame = tk.Frame(left_frame) btn_frame.pack(fill=tk.X, pady=(0, 10)) tk.Button(btn_frame, text="识别", command=self.recognize, width=8).pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="清除", command=self.clear_canvas, width=8).pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="样本", command=self.show_samples, width=8).pack(side=tk.LEFT, padx=5) # 右侧结果区域 right_frame = tk.Frame(main_frame) right_frame.grid(row=1, column=1, padx=5, pady=5, sticky="nsew") # 识别结果 result_frame = tk.LabelFrame(right_frame, text="识别结果", font=("Arial", 10, "bold")) result_frame.pack(fill=tk.X, padx=5, pady=5) self.result_label = tk.Label( result_frame, text="请绘制数字", font=("Arial", 24), pady=10 ) self.result_label.pack() self.prob_label = tk.Label( result_frame, text="", font=("Arial", 12) ) self.prob_label.pack() # 置信度可视化 confidence_frame = tk.LabelFrame(right_frame, text="识别置信度", font=("Arial", 10, "bold")) confidence_frame.pack(fill=tk.X, padx=5, pady=5) self.confidence_canvas = tk.Canvas( confidence_frame, bg="white", height=50 ) self.confidence_canvas.pack(fill=tk.X, padx=10, pady=10) self.confidence_canvas.create_text( 150, 25, text="识别后显示置信度", fill="gray", font=("Arial", 10) ) # 候选数字 candidates_frame = tk.LabelFrame(right_frame, text="可能的数字", font=("Arial", 10, "bold")) candidates_frame.pack(fill=tk.X, padx=5, pady=5) columns = ("数字", "概率") self.candidates_tree = ttk.Treeview( candidates_frame, columns=columns, show="headings", height=4 ) for col in columns: self.candidates_tree.heading(col, text=col) self.candidates_tree.column(col, width=80, anchor=tk.CENTER) scrollbar = ttk.Scrollbar( candidates_frame, orient=tk.VERTICAL, command=self.candidates_tree.yview ) self.candidates_tree.configure(yscroll=scrollbar.set) self.candidates_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5) scrollbar.pack(side=tk.RIGHT, fill=tk.Y, padx=5, pady=5) # 第三行:模型性能对比和训练集管理 # 模型性能对比 performance_frame = tk.LabelFrame(main_frame, text="模型性能对比", font=("Arial", 10, "bold")) performance_frame.grid(row=2, column=0, padx=5, pady=5, sticky="nsew") columns = ("模型名称", "准确率") self.performance_tree = ttk.Treeview( performance_frame, columns=columns, show="headings", height=8 ) for col in columns: self.performance_tree.heading(col, text=col) self.performance_tree.column(col, width=120, anchor=tk.CENTER) scrollbar = ttk.Scrollbar( performance_frame, orient=tk.VERTICAL, command=self.performance_tree.yview ) self.performance_tree.configure(yscroll=scrollbar.set) self.performance_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5) scrollbar.pack(side=tk.RIGHT, fill=tk.Y, padx=5, pady=5) # 训练集管理 train_frame = tk.LabelFrame(main_frame, text="训练集管理", font=("Arial", 10, "bold")) train_frame.grid(row=2, column=1, padx=5, pady=5, sticky="nsew") # 使用grid布局训练集管理按钮 tk.Button( train_frame, text="保存为训练样本", command=self.save_as_training_sample, width=18, height=2 ).grid(row=0, column=0, padx=5, pady=5, sticky="ew") tk.Button( train_frame, text="保存全部训练集", command=self.save_all_training_data, width=18, height=2 ).grid(row=0, column=1, padx=5, pady=5, sticky="ew") tk.Button( train_frame, text="加载训练集", command=self.load_training_data, width=18, height=2 ).grid(row=1, column=0, padx=5, pady=5, sticky="ew") tk.Button( train_frame, text="性能图表", command=self.show_performance_chart, width=18, height=2 ).grid(row=1, column=1, padx=5, pady=5, sticky="ew") # 状态信息 self.status_var = tk.StringVar(value="就绪") status_bar = tk.Label( self.root, textvariable=self.status_var, bd=1, relief=tk.SUNKEN, anchor=tk.W, font=("Arial", 10) ) status_bar.pack(side=tk.BOTTOM, fill=tk.X) # 配置权重 main_frame.grid_columnconfigure(0, weight=1) main_frame.grid_columnconfigure(1, weight=1) main_frame.grid_rowconfigure(1, weight=1) main_frame.grid_rowconfigure(2, weight=1) def start_draw(self, event): """开始绘制""" self.drawing = True self.last_x, self.last_y = event.x, event.y def draw(self, event): """绘制""" if not self.drawing: return x, y = event.x, event.y # 在画布上绘制 self.canvas.create_line( self.last_x, self.last_y, x, y, fill="black", width=self.BRUSH_SIZE, capstyle=tk.ROUND, smooth=True ) # 在图像上绘制 self.draw_obj.line( [self.last_x, self.last_y, x, y], fill=0, width=self.BRUSH_SIZE ) self.last_x, self.last_y = x, y def stop_draw(self, event): """停止绘制""" self.drawing = False self.has_drawn = True self.status_var.set("已绘制数字,点击'识别'进行识别") def clear_canvas(self): """清除画布""" self.canvas.delete("all") self.image = Image.new("L", (self.CANVAS_SIZE, self.CANVAS_SIZE), 255) self.draw_obj = ImageDraw.Draw(self.image) # 添加绘制提示 self.canvas.create_text( self.CANVAS_SIZE / 2, self.CANVAS_SIZE / 2, text="绘制数字", fill="gray", font=("Arial", 16) ) self.result_label.config(text="请绘制数字") self.prob_label.config(text="") self.clear_confidence_display() self.has_drawn = False self.status_var.set("画布已清除") def clear_confidence_display(self): """清除置信度显示""" self.confidence_canvas.delete("all") self.confidence_canvas.create_text( 150, 25, text="识别后显示置信度", fill="gray", font=("Arial", 10) ) for item in self.candidates_tree.get_children(): self.candidates_tree.delete(item) def preprocess_image(self): """预处理手写数字图像""" img_array = np.array(self.image) # 高斯模糊降噪 img_array = cv2.GaussianBlur(img_array, (5, 5), 0) # 二化 _, img_array = cv2.threshold(img_array, 127, 255, cv2.THRESH_BINARY_INV) # 轮廓检测 contours, _ = cv2.findContours(img_array, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: self.status_var.set("未检测到有效数字,请重新绘制") return None # 找到最大轮廓 c = max(contours, key=cv2.contourArea) x, y, w, h = cv2.boundingRect(c) # 提取数字区域 digit = img_array[y:y+h, x:x+w] # 填充为正方形 size = max(w, h) padded = np.ones((size, size), dtype=np.uint8) * 255 offset_x = (size - w) // 2 offset_y = (size - h) // 2 padded[offset_y:offset_y+h, offset_x:offset_x+w] = digit # 缩放为8x8 resized = cv2.resize(padded, (8, 8), interpolation=cv2.INTER_AREA) # 归一化 normalized = 16 - (resized / 255 * 16).astype(np.uint8) return normalized.flatten() def recognize(self): """识别手写数字""" if not self.has_drawn: self.status_var.set("请先绘制数字再识别") return if self.current_model is None: self.status_var.set("模型未加载,请选择模型") return # 预处理图像 img_array = self.preprocess_image() if img_array is None: return img_input = img_array.reshape(1, -1) try: # 标准化 if self.scaler: img_input = self.scaler.transform(img_input) # LightGBM特殊处理 if self.current_model_type == 'lgb' and hasattr(self.current_model, 'feature_name_'): img_input = pd.DataFrame(img_input, columns=self.current_model.feature_name_) # 预测 pred = self.current_model.predict(img_input)[0] self.result_label.config(text=f"识别结果: {pred}") # 概率预测 if hasattr(self.current_model, 'predict_proba'): probs = self.current_model.predict_proba(img_input)[0] confidence = probs[pred] # 更新UI self.prob_label.config(text=f"置信度: {confidence:.2%}") self.update_confidence_display(confidence) # 显示候选数字 top3 = sorted(enumerate(probs), key=lambda x: -x[1])[:3] self.update_candidates_display(top3) else: self.prob_label.config(text="该模型不支持概率输出") self.clear_confidence_display() self.status_var.set(f"识别完成: 数字 {pred}") except Exception as e: self.status_var.set(f"识别错误: {str(e)}") self.clear_confidence_display() def update_confidence_display(self, confidence): """更新置信度可视化""" self.confidence_canvas.delete("all") # 画布尺寸 canvas_width = self.confidence_canvas.winfo_width() or 300 # 绘制背景 self.confidence_canvas.create_rectangle( 10, 10, canvas_width - 10, 40, fill="#f0f0f0", outline="#cccccc" ) # 绘制置信度条 bar_width = int((canvas_width - 20) * confidence) color = self.get_confidence_color(confidence) self.confidence_canvas.create_rectangle( 10, 10, 10 + bar_width, 40, fill=color, outline="" ) # 绘制文本 self.confidence_canvas.create_text( canvas_width / 2, 25, text=f"{confidence:.1%}", font=("Arial", 10, "bold") ) # 绘制刻度 for i in range(0, 11): x_pos = 10 + i * (canvas_width - 20) / 10 self.confidence_canvas.create_line(x_pos, 40, x_pos, 45, width=1) if i % 2 == 0: self.confidence_canvas.create_text(x_pos, 55, text=f"{i*10}%", font=("Arial", 8)) def get_confidence_color(self, confidence): """根据置信度获取颜色""" if confidence >= 0.9: return "#4CAF50" # 绿色 elif confidence >= 0.7: return "#FFC107" # 黄色 else: return "#F44336" # 红色 def update_candidates_display(self, candidates): """更新候选数字显示""" # 清空现有项 for item in self.candidates_tree.get_children(): self.candidates_tree.delete(item) # 添加新项 for digit, prob in candidates: self.candidates_tree.insert( "", tk.END, values=(digit, f"{prob:.2%}") ) def show_samples(self): """显示样本图像""" plt.figure(figsize=(10, 4)) for i in range(10): plt.subplot(2, 5, i+1) sample_idx = np.where(self.digits.target == i)[0][0] plt.imshow(self.digits.images[sample_idx], cmap="gray") plt.title(f"数字 {i}", fontsize=9) plt.axis("off") plt.tight_layout() plt.show() def on_model_select(self, event): """模型选择事件处理""" selected_name = self.model_var.get() model_type = next( (k for k, v in self.available_models if v == selected_name), None ) if model_type: self.change_model(model_type) def change_model(self, model_type): """切换模型""" model_name = MODEL_METADATA[model_type][0] # 从缓存加载 if model_type in self.model_cache: self.current_model, self.scaler, accuracy, self.current_model_type = self.model_cache[model_type] self.model_label.config(text=f"{model_name} (准确率:{accuracy:.4f})") self.status_var.set(f"已加载模型: {model_name}") return self.status_var.set(f"正在加载模型: {model_name}...") self.root.update() # 更新UI显示状态 try: X_train, X_test, y_train, y_test = self.model_factory.get_split_data(self.digits) self.current_model, self.scaler, accuracy = self.model_factory.train_and_evaluate( model_type, X_train, y_train, X_test, y_test ) self.current_model_type = model_type self.model_cache[model_type] = (self.current_model, self.scaler, accuracy, self.current_model_type) self.model_label.config(text=f"{model_name} (准确率:{accuracy:.4f})") self.status_var.set(f"模型加载完成: {model_name}, 准确率: {accuracy:.4f}") self.clear_canvas() # 更新性能表格 self.load_performance_data() except Exception as e: self.status_var.set(f"模型加载失败: {str(e)}") self.model_label.config(text="模型加载失败") def init_default_model(self): """初始化默认模型""" self.model_var.set(self.available_models[0][1]) self.change_model(self.available_models[0][0]) def load_performance_data(self): """加载性能数据""" results = self.model_factory.evaluate_all_models(self.digits) # 清空表格 for item in self.performance_tree.get_children(): self.performance_tree.delete(item) # 添加数据 for i, result in enumerate(results): tag = "highlight" if i == 0 else "" self.performance_tree.insert( "", tk.END, values=(result["模型名称"], result["准确率"]), tags=(tag,) ) self.performance_tree.tag_configure("highlight", background="#e6f7ff") def show_performance_chart(self): """显示性能图表""" results = self.model_factory.evaluate_all_models(self.digits) # 提取有效结果 valid_results = [] for result in results: try: accuracy = float(result["准确率"]) valid_results.append((result["模型名称"], accuracy)) except ValueError: continue if not valid_results: messagebox.showinfo("提示", "没有可用的性能数据") return # 排序 valid_results.sort(key=lambda x: x[1], reverse=True) models, accuracies = zip(*valid_results) # 创建图表 plt.figure(figsize=(10, 5)) bars = plt.barh(models, accuracies, color='#2196F3') plt.xlabel('准确率', fontsize=10) plt.ylabel('模型', fontsize=10) plt.title('模型性能对比', fontsize=12) plt.xlim(0, 1.05) # 添加数标签 for bar in bars: width = bar.get_width() plt.text( width + 0.01, bar.get_y() + bar.get_height()/2, f'{width:.4f}', ha='left', va='center', fontsize=8 ) plt.tight_layout() plt.show() def save_as_training_sample(self): """保存为训练样本""" if not self.has_drawn: self.status_var.set("请先绘制数字再保存") return img_array = self.preprocess_image() if img_array is None: return # 弹出标签输入窗口 label_window = tk.Toplevel(self.root) label_window.title("输入标签") label_window.geometry("300x150") label_window.transient(self.root) label_window.grab_set() tk.Label( label_window, text="请输入数字标签 (0-9):", font=("Arial", 10) ).pack(pady=10) entry = tk.Entry(label_window, font=("Arial", 12), width=5) entry.pack(pady=5) entry.focus_set() def save_with_label(): try: label = int(entry.get()) if label < 0 or label > 9: raise ValueError("标签必须是0-9的数字") self.custom_data.append((img_array.tolist(), label)) self.status_var.set(f"已保存数字 {label} (共 {len(self.custom_data)} 个样本)") label_window.destroy() except ValueError as e: self.status_var.set(f"保存错误: {str(e)}") tk.Button( label_window, text="保存", command=save_with_label, width=10 ).pack(pady=5) def save_all_training_data(self): """保存全部训练数据""" if not self.custom_data: self.status_var.set("没有训练数据可保存") return file_path = filedialog.asksaveasfilename( defaultextension=".csv", filetypes=[("CSV文件", "*.csv")], initialfile="custom_digits.csv", title="保存训练集" ) if not file_path: return try: with open(file_path, 'w', newline='', encoding='utf-8') as f: writer = csv.writer(f) writer.writerow([f'pixel{i}' for i in range(64)] + ['label']) for img_data, label in self.custom_data: writer.writerow(img_data + [label]) self.status_var.set(f"已保存 {len(self.custom_data)} 个样本到 {os.path.basename(file_path)}") except Exception as e: self.status_var.set(f"保存失败: {str(e)}") def load_training_data(self): """加载训练数据""" file_path = filedialog.askopenfilename( filetypes=[("CSV文件", "*.csv")], title="加载训练集" ) if not file_path: return try: self.custom_data = [] with open(file_path, 'r', newline='', encoding='utf-8') as f: reader = csv.reader(f) next(reader) # 跳过标题 for row in reader: if len(row) != 65: continue img_data = [float(pixel) for pixel in row[:64]] label = int(row[64]) self.custom_data.append((img_data, label)) self.status_var.set(f"已加载 {len(self.custom_data)} 个样本") except Exception as e: self.status_var.set(f"加载失败: {str(e)}") def run(self): """运行应用""" self.root.mainloop() if __name__ == "__main__": digits = load_digits() root = tk.Tk() app = HandwritingBoard(root, ModelFactory, digits) app.run() 基于此代码,在其中做好大量注释,同时要明确代码的分区功能,要显示明白,让刚学python的同学要能看懂。
06-24
那我是都删掉吗?还是说MapFragment这个用上会更好?目前代码如下:package com.example.bus.ui.map; import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.ArrayAdapter; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; // 👉 新增导入:用于动态修改约束 import androidx.constraintlayout.widget.ConstraintSet; import androidx.constraintlayout.widget.ConstraintLayout; // 🔺 新增导入:用于设置 Marker 颜色 import com.amap.api.maps.model.BitmapDescriptorFactory; import com.amap.api.maps.AMap; import com.amap.api.maps.CameraUpdateFactory; import com.amap.api.maps.MapView; import com.amap.api.maps.UiSettings; import com.amap.api.maps.model.LatLng; import com.amap.api.maps.model.Marker; import com.amap.api.maps.model.MarkerOptions; import com.amap.api.maps.model.MyLocationStyle; import com.amap.api.services.core.LatLonPoint; import com.amap.api.services.core.PoiItem; import com.amap.api.services.poisearch.PoiResult; import com.amap.api.services.poisearch.PoiSearch; import com.amap.api.services.geocoder.GeocodeSearch; import com.amap.api.services.geocoder.RegeocodeQuery; import com.amap.api.services.geocoder.RegeocodeResult; import com.example.bus.R; import com.example.bus.RealTimePoiSuggestHelper; import com.example.bus.RoutePlanActivity; import com.example.bus.ResultAdapter; import com.example.bus.databinding.FragmentMapBinding; import java.util.ArrayList; import java.util.List; // ✅ 新增导入 import android.view.inputmethod.EditorInfo; public class MapFragment extends Fragment implements PoiSearch.OnPoiSearchListener, GeocodeSearch.OnGeocodeSearchListener { private FragmentMapBinding binding; private MapView mapView; private AMap aMap; // 数据 private List<PoiItem> poiList = new ArrayList<>(); private ResultAdapter adapter; private PoiSearch poiSearch; // 当前阶段:1=选择起点, 2=选择终点 private int selectionStage = 0; // 缓存已选 POI private PoiItem selectedStartPoi = null; private PoiItem selectedEndPoi = null; private Marker startMarker = null; private Marker endMarker = null; // 缓存关键词 private String lastStartKeyword = ""; private String lastEndKeyword = ""; // ✅ 当前城市 private String currentCity = "全国"; private static final int LOCATION_PERMISSION_REQUEST_CODE = 1001; // ✅ 标记是否已居中我的位置 private boolean userHasInteracted = false; // ✅ 反地理编码 private GeocodeSearch geocodeSearch; // 【关键新增】保存定位得到的“我的位置” private double myCurrentLat = 0; private double myCurrentLng = 0; private boolean isLocationReady = false; // 定位是否完成 @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentMapBinding.inflate(inflater, container, false); View root = binding.getRoot(); mapView = binding.mapView; mapView.onCreate(savedInstanceState); initViews(); setupMap(savedInstanceState); setupSearchSuggestion(); // 包含输入建议、防抖、回车事件 return root; } private void initViews() { adapter = new ResultAdapter(poiList, this::onPoiItemSelected); binding.resultList.setLayoutManager(new LinearLayoutManager(requireContext())); binding.resultList.setAdapter(adapter); binding.mapSearch.setOnClickListener(v -> performSearch()); binding.btnSwitchTarget.setOnClickListener(v -> { if (selectionStage == 1) { showEndpointSelection(binding.mapInput2.getText().toString().trim()); } else if (selectionStage == 2) { showStartpointSelection(binding.mapInput1.getText().toString().trim()); } }); binding.btnGoTo.setOnClickListener(v -> { if (selectedStartPoi != null && selectedEndPoi != null) { Intent intent = new Intent(requireContext(), RoutePlanActivity.class); intent.putExtra(RoutePlanActivity.EXTRA_SOURCE, RoutePlanActivity.SOURCE_FROM_MAP_DIRECT); intent.putExtra("start_lat", selectedStartPoi.getLatLonPoint().getLatitude()); intent.putExtra("start_lng", selectedStartPoi.getLatLonPoint().getLongitude()); intent.putExtra("target_lat", selectedEndPoi.getLatLonPoint().getLatitude()); intent.putExtra("target_lng", selectedEndPoi.getLatLonPoint().getLongitude()); intent.putExtra("target_title", selectedEndPoi.getTitle()); startActivity(intent); } else { Toast.makeText(requireContext(), "请完成起点和终点的选择", Toast.LENGTH_SHORT).show(); } }); } private void performSearch() { String startKeyword = binding.mapInput1.getText().toString().trim(); String endKeyword = binding.mapInput2.getText().toString().trim(); if (startKeyword.isEmpty()) { Toast.makeText(requireContext(), "请输入起点", Toast.LENGTH_SHORT).show(); return; } if (endKeyword.isEmpty()) { Toast.makeText(requireContext(), "请输入终点", Toast.LENGTH_SHORT).show(); return; } // 智能判断跳过搜索 if (startKeyword.equals(lastStartKeyword) && endKeyword.equals(lastEndKeyword) && selectedStartPoi != null && selectedEndPoi != null) { binding.btnGoTo.performClick(); return; } // 展示 UI binding.containerResultList.setVisibility(View.VISIBLE); binding.buttonGroup.setVisibility(View.VISIBLE); // 动态压缩地图区域 ConstraintSet constraintSet = new ConstraintSet(); constraintSet.clone((ConstraintLayout) binding.getRoot()); constraintSet.connect( R.id.map_view, ConstraintSet.BOTTOM, R.id.container_result_list, ConstraintSet.TOP, 0 ); constraintSet.applyTo((ConstraintLayout) binding.getRoot()); userHasInteracted = true; // 隐藏软键盘 View currentFocus = requireActivity().getCurrentFocus(); if (currentFocus != null) { currentFocus.clearFocus(); InputMethodManager imm = (InputMethodManager) requireContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(currentFocus.getWindowToken(), 0); } // 执行搜索 if (!startKeyword.equals(lastStartKeyword)) { lastStartKeyword = startKeyword; lastEndKeyword = endKeyword; showStartpointSelection(startKeyword); } else if (!endKeyword.equals(lastEndKeyword)) { lastEndKeyword = endKeyword; showEndpointSelection(endKeyword); } else if (selectedStartPoi == null) { showStartpointSelection(startKeyword); } else { showEndpointSelection(endKeyword); } } // ✅ 显示“搜索起点中...” private void showStartpointSelection(String keyword) { selectionStage = 1; binding.btnSwitchTarget.setText("前往选择终点"); binding.btnGoTo.setEnabled(false); binding.emptyView.setText("🔍 搜索起点中..."); binding.emptyView.setVisibility(View.VISIBLE); binding.resultList.setVisibility(View.GONE); doSearch(keyword); } // ✅ 显示“搜索终点中...” private void showEndpointSelection(String keyword) { selectionStage = 2; binding.btnSwitchTarget.setText("回到选择起点"); binding.btnGoTo.setEnabled(false); binding.emptyView.setText("🔍 搜索终点中..."); binding.emptyView.setVisibility(View.VISIBLE); binding.resultList.setVisibility(View.GONE); doSearch(keyword); } private void doSearch(String keyword) { if (keyword.isEmpty()) return; PoiSearch.Query query = new PoiSearch.Query(keyword, "", currentCity); query.setPageSize(20); query.setPageNum(0); // ✅ 使用缓存的位置(仅当真实定位完成后) if (isLocationReady) { LatLonPoint lp = new LatLonPoint(myCurrentLat, myCurrentLng); query.setLocation(lp); // 让高德根据“我在哪”智能排序 } try { poiSearch = new PoiSearch(requireContext(), query); poiSearch.setOnPoiSearchListener(this); poiSearch.searchPOIAsyn(); } catch (Exception e) { e.printStackTrace(); Toast.makeText(requireContext(), "搜索失败", Toast.LENGTH_SHORT).show(); } } private void onPoiItemSelected(PoiItem item) { LatLng latLng = new LatLng(item.getLatLonPoint().getLatitude(), item.getLatLonPoint().getLongitude()); if (selectionStage == 1) { if (startMarker != null) { startMarker.remove(); startMarker = null; } startMarker = aMap.addMarker(new MarkerOptions() .position(latLng) .title("起点:" + item.getTitle()) .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_GREEN))); selectedStartPoi = item; aMap.animateCamera(CameraUpdateFactory.newLatLngZoom(latLng, 14f)); updateGoToButtonState(); } else if (selectionStage == 2) { if (endMarker != null) { endMarker.remove(); endMarker = null; } endMarker = aMap.addMarker(new MarkerOptions() .position(latLng) .title("终点:" + item.getTitle()) .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_RED))); selectedEndPoi = item; aMap.animateCamera(CameraUpdateFactory.newLatLngZoom(latLng, 14f)); updateGoToButtonState(); } userHasInteracted = true; } private void updateGoToButtonState() { binding.btnGoTo.setEnabled(selectedStartPoi != null && selectedEndPoi != null); } @Override public void onPoiSearched(PoiResult result, int rCode) { if (rCode == 1000 && result != null && result.getPois() != null && !result.getPois().isEmpty()) { List<PoiItem> newPoiList = result.getPois(); poiList.clear(); poiList.addAll(newPoiList); if (adapter == null) { adapter = new ResultAdapter(poiList, this::onPoiItemSelected); binding.resultList.setLayoutManager(new LinearLayoutManager(requireContext())); binding.resultList.setAdapter(adapter); } else { adapter.notifyDataSetChanged(); binding.resultList.scrollToPosition(0); } binding.emptyView.setVisibility(View.GONE); binding.resultList.setVisibility(View.VISIBLE); // 先设置 Adapter 的选中状态(触发 UI 高亮) adapter.setSelected(0); // 自动选中第一个 onPoiItemSelected(poiList.get(0)); } else { if (selectionStage == 1) { binding.emptyView.setText("⚠️ 未找到相关起点"); } else if (selectionStage == 2) { binding.emptyView.setText("⚠️ 未找到相关终点"); } binding.emptyView.setVisibility(View.VISIBLE); binding.resultList.setVisibility(View.GONE); } } @Override public void onPoiItemSearched(PoiItem item, int rCode) { // 必须实现 } private void setupMap(Bundle savedInstanceState) { mapView.onCreate(savedInstanceState); aMap = mapView.getMap(); if (aMap != null) { initMapSettings(); } else { new Handler(Looper.getMainLooper()).post(() -> { aMap = mapView.getMap(); if (aMap != null) { initMapSettings(); } else { waitAMapReady(); } }); } try { geocodeSearch = new GeocodeSearch(requireContext()); geocodeSearch.setOnGeocodeSearchListener(this); } catch (Exception e) { e.printStackTrace(); } } private void waitAMapReady() { new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { int retry = 0; @Override public void run() { if (mapView == null) return; aMap = mapView.getMap(); if (aMap != null) { initMapSettings(); } else if (retry++ < 30) { new Handler(Looper.getMainLooper()).postDelayed(this, 100); } } }, 100); } private void initMapSettings() { UiSettings uiSettings = aMap.getUiSettings(); uiSettings.setZoomControlsEnabled(true); uiSettings.setCompassEnabled(true); uiSettings.setScrollGesturesEnabled(true); uiSettings.setMyLocationButtonEnabled(true); new Handler(Looper.getMainLooper()).post(() -> aMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(39.909186, 116.397411), 10f)) ); enableMyLocationLayer(); } private void enableMyLocationLayer() { if (aMap == null) return; if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { MyLocationStyle myLocationStyle = new MyLocationStyle(); myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE_NO_CENTER); aMap.setMyLocationStyle(myLocationStyle); aMap.setMyLocationEnabled(true); AMap.OnMyLocationChangeListener listener = location -> { if (location != null && !userHasInteracted) { LatLng curLatlng = new LatLng(location.getLatitude(), location.getLongitude()); // ✅ 记录真实位置 myCurrentLat = location.getLatitude(); myCurrentLng = location.getLongitude(); isLocationReady = true; aMap.animateCamera(CameraUpdateFactory.newLatLngZoom(curLatlng, 16f)); userHasInteracted = true; // 获取城市 LatLonPoint point = new LatLonPoint(myCurrentLat, myCurrentLng); RegeocodeQuery query = new RegeocodeQuery(point, 200, GeocodeSearch.AMAP); try { geocodeSearch.getFromLocationAsyn(query); } catch (Exception e) { e.printStackTrace(); } aMap.setOnMyLocationChangeListener(null); } }; aMap.setOnMyLocationChangeListener(listener); } else { ActivityCompat.requestPermissions(requireActivity(), new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, LOCATION_PERMISSION_REQUEST_CODE); } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == LOCATION_PERMISSION_REQUEST_CODE) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (aMap != null) { MyLocationStyle myLocationStyle = new MyLocationStyle(); myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE_NO_CENTER); aMap.setMyLocationStyle(myLocationStyle); aMap.setMyLocationEnabled(true); AMap.OnMyLocationChangeListener listener = location -> { if (location != null && !userHasInteracted) { LatLng curLatlng = new LatLng(location.getLatitude(), location.getLongitude()); // ✅ 同步记录位置 myCurrentLat = location.getLatitude(); myCurrentLng = location.getLongitude(); isLocationReady = true; aMap.animateCamera(CameraUpdateFactory.newLatLngZoom(curLatlng, 16f)); userHasInteracted = true; LatLonPoint point = new LatLonPoint(myCurrentLat, myCurrentLng); RegeocodeQuery query = new RegeocodeQuery(point, 200, GeocodeSearch.AMAP); try { geocodeSearch.getFromLocationAsyn(query); } catch (Exception e) { e.printStackTrace(); } aMap.setOnMyLocationChangeListener(null); } }; aMap.setOnMyLocationChangeListener(listener); } } else { Toast.makeText(requireContext(), "定位权限被拒绝,部分功能受限", Toast.LENGTH_LONG).show(); currentCity = "全国"; } } } @Override public void onRegeocodeSearched(RegeocodeResult result, int rCode) { if (rCode == 1000 && result != null && result.getRegeocodeAddress() != null) { String city = result.getRegeocodeAddress().getCity(); currentCity = (city != null && !city.isEmpty()) ? city : result.getRegeocodeAddress().getProvince(); } else { currentCity = "全国"; } } @Override public void onGeocodeSearched(com.amap.api.services.geocoder.GeocodeResult geocodeResult, int i) { // 忽略 } @Override public void onResume() { super.onResume(); mapView.onResume(); if (!userHasInteracted) { enableMyLocationLayer(); } } @Override public void onPause() { super.onPause(); mapView.onPause(); } @Override public void onDestroyView() { super.onDestroyView(); mapView.onDestroy(); geocodeSearch = null; binding = null; } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); mapView.onSaveInstanceState(outState); } // ✅ 完整保留原始的 setupSearchSuggestion 实现 private void setupSearchSuggestion() { // 创建实时 POI 建议助手 RealTimePoiSuggestHelper suggestHelper = new RealTimePoiSuggestHelper(requireContext()); suggestHelper.setCurrentCity(currentCity); suggestHelper.setCallback(suggestions -> { if (suggestions.length > 0) { ArrayAdapter<String> adapter = new ArrayAdapter<>( requireContext(), android.R.layout.simple_dropdown_item_1line, suggestions ); new Handler(Looper.getMainLooper()).post(() -> { binding.mapInput1.setAdapter(adapter); binding.mapInput2.setAdapter(adapter); if (requireActivity().getCurrentFocus() == binding.mapInput1) { binding.mapInput1.showDropDown(); } else if (requireActivity().getCurrentFocus() == binding.mapInput2) { binding.mapInput2.showDropDown(); } }); } else { // 不清空 adapter,保留上次有效结果 } }); Handler handler = new Handler(Looper.getMainLooper()); Runnable[] pending1 = {null}, pending2 = {null}; // mapInput1 输入监听 binding.mapInput1.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void onTextChanged(CharSequence s, int start, int before, int count) { if (pending1[0] != null) handler.removeCallbacks(pending1[0]); if (s.length() == 0) { binding.mapInput1.setAdapter(null); return; } pending1[0] = () -> suggestHelper.requestSuggestions(s.toString()); handler.postDelayed(pending1[0], 300); // 防抖 300ms } @Override public void afterTextChanged(Editable s) {} }); // mapInput2 输入监听 binding.mapInput2.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void onTextChanged(CharSequence s, int start, int before, int count) { if (pending2[0] != null) handler.removeCallbacks(pending2[0]); if (s.length() == 0) { binding.mapInput2.setAdapter(null); return; } pending2[0] = () -> suggestHelper.requestSuggestions(s.toString()); handler.postDelayed(pending2[0], 300); } @Override public void afterTextChanged(Editable s) {} }); // 回车事件保持不 binding.mapInput1.setOnEditorActionListener((v, actionId, event) -> { if ((actionId & EditorInfo.IME_MASK_ACTION) == EditorInfo.IME_ACTION_SEARCH) { performSearch(); return true; } return false; }); binding.mapInput2.setOnEditorActionListener((v, actionId, event) -> { if ((actionId & EditorInfo.IME_MASK_ACTION) == EditorInfo.IME_ACTION_SEARCH) { performSearch(); return true; } return false; }); } private void handleSearchError(int rCode) { String msg; switch (rCode) { case 12: msg = "API Key 错误"; break; case 27: msg = "网络连接失败"; break; case 30: msg = "SHA1 或包名错误"; break; case 33: msg = "请求频繁"; break; default: msg = "搜索失败: " + rCode; break; } Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show(); } // ✅ 自定义 TextWatcher 类,保留原始写法 private static class SimpleTextWatcher implements android.text.TextWatcher { private final java.util.function.Consumer<CharSequence> onTextChanged; public SimpleTextWatcher(java.util.function.Consumer<CharSequence> onTextChanged) { this.onTextChanged = onTextChanged; } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void afterTextChanged(Editable s) {} @Override public void onTextChanged(CharSequence s, int start, int before, int count) { onTextChanged.accept(s); } } } 如果用上又该怎么用?
最新发布
12-02
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

【拾光静好 微微一笑】

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值