众所周知,在MFC编程模式下,我们的应用程序界面是由窗口和控件组成,实际上,控件也是窗口,是一种特殊的窗口。而MFC已经为我们准备了一些基础的控件,例如:按钮,编辑框,and so on,但是在我们的大型软件开发中,这些控件是不够的。
本文开发一种画图控件为导向,来说明其开发方法。
查看MFC源码,我们发现,按钮控件(Cbutton)是公有继承于 CWnd类的,如下面的代码:(只摘抄了一部分)
class CButton : public CWnd
{
DECLARE_DYNAMIC(CButton)
// Constructors
public:
CButton();
virtual BOOL Create(LPCTSTR lpszCaption, DWORD dwStyle,
const RECT& rect, CWnd* pParentWnd, UINT nID);
};
一般我们在窗口增加控件时,最常用的是可视化操作——也就是直接用鼠标拖拽。还有一种是动态创建,也就是用上面的Create(),(不过比较繁杂,用的较少)。
下面直接给出我开发的控件代码:
// Header Files
class CMapping2 : public CWnd
{
public:
CMapping2();
~CMapping2();
public:
DECLARE_MESSAGE_MAP()
afx_msg void OnDestroy();
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
afx_msg void OnPaint();
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
afx_msg void OnRButtonDown(UINT nFlags, CPoint point);
afx_msg void OnMouseMove(UINT nFlags, CPoint point);
afx_msg BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt);
public:
int SetGrainZoomRatio(int nZoomRatio);
int GetGrainZoomRatio();
int SetWaferSize(int nCols, int nRows);
CPoint getWaferSize();
int DrawSingleGrain(int nCols, int nRows, COLORREF clr, BOOL bIsRefresh = TRUE);
int DrawBatchGrain(CPoint Start, CPoint End, COLORREF clr);
int ClearSingleGrain(int nCols, int nRows/*, COLORREF clr*/);
int ClearAll();
int UpdateMapping();
int SetFunctionFlag(int nIndex);
int SetLegend(int nCol, int nRow);
int SaveMapping(LPCTSTR lpszPath, BOOL bIsClip = FALSE); // xlsx and csv format
int LoadMapping(LPCTSTR lpszPath);
private:
int DrawLegendX(int nCol, CString str);
int DrawLegendY(int nRow, CString str);
HWND GetParentHWnd();
int MakeSurePathExist(CString strPath);
int GetGrainCoordinate(const CPoint PixelCoordinates, CPoint& GrainCoordinates);
public:
enum{BACKGROUND_GRAY=RGB(250, 250, 250),
BAD_COLOR=RGB(255, 0, 0),
GOOD_COLOR=RGB(0, 255, 0),
SELECT_COLOR = RGB(128, 128, 128),
LEGEND_BACKGROUND = RGB(200, 200, 200)};
enum{DRAG = 0, BATCH_SELECT};
private:
// 0 1 2 3 4 5...
//拖动 批量选择
static const int m_snMaxItem = 10;
BOOL m_IsSelected[m_snMaxItem];
CPoint m_StartSelect;
CPoint m_EndSelect;
// 选择
CPoint m_SltStartPt;
CPoint m_SltEndPt;
private:
int m_nWidth; // 去除边框的有效区域
int m_nHeight;
int m_nMemSize;
int m_nClipOrgX; // 双缓冲裁剪坐标原点 左上角
int m_nClipOrgY;
int m_nGrainGap;
int m_nBorder;
int m_nZoomRatio;
int m_nMaxZoom;
int m_nDieCols;
int m_nDieRows;
int m_nJump;
private:
// Mapping
CBitmap m_bitmap;
CBitmap *m_pOldBitmap;
CDC *m_pDC;
CDC m_MemDC;
CRect m_rect;
// top
CBitmap m_Topbitmap;
CDC m_TopMemDC;
CFont *pTopOldFont;
// right
CBitmap m_Rightbitmap;
CDC m_RightMemDC;
CFont *pRightOldFont;
private:
BOOL m_bLBtnIsDown;
BOOL m_bIsDrag;
CPoint m_StartPt;
private:
CMainFrame *m_pMainFrame;
};
// Source Files
CMapping2::CMapping2()
{
m_nWidth = 0;
m_nHeight = 0;
m_nGrainGap = 2;
m_nBorder = 20;
m_nZoomRatio = 10;
m_nMaxZoom = 15;
m_nMemSize = 2000; // 2000x2000 像素点
m_nClipOrgX = 1000;
m_nClipOrgY = 1000;
m_nJump = 10;
m_bLBtnIsDown = FALSE;
m_bIsDrag = FALSE;
// 这些功能选择都是互斥项
for(int i = 0; i != m_snMaxItem; i++)
{
m_IsSelected[i] = FALSE;
}
// 默认开启拖动功能
m_IsSelected[DRAG] = TRUE;
}
CMapping2::~CMapping2()
{
}
BEGIN_MESSAGE_MAP(CMapping2, CWnd)
ON_WM_LBUTTONDOWN()
ON_WM_LBUTTONUP()
ON_WM_RBUTTONDOWN()
ON_WM_MOUSEMOVE()
ON_WM_MOUSEWHEEL()
ON_WM_PAINT()
ON_WM_DESTROY()
ON_WM_CREATE()
END_MESSAGE_MAP()
void CMapping2::OnPaint()
{
CPaintDC dc(this);
dc.BitBlt(m_nBorder, m_nBorder, m_nWidth, m_nHeight, &m_MemDC, m_nClipOrgX, m_nClipOrgY, SRCCOPY);
dc.BitBlt(m_nBorder, 0, m_nWidth, m_nBorder, &m_TopMemDC, m_nClipOrgX, 0, SRCCOPY);
dc.BitBlt(m_nWidth + m_nBorder, m_nBorder, m_nBorder, m_nHeight, &m_RightMemDC, 0, m_nClipOrgY, SRCCOPY);
}
void CMapping2::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CRect rect;
GetClientRect(rect);
rect.left += 10;
rect.right -= 10;
rect.top += 10;
rect.bottom -= 10;
if(!rect.PtInRect(point))
{
// ReleaseCapture();
return;
}
SetCapture();
// 拖动需要
m_bLBtnIsDown = TRUE;
m_StartPt = point;
CPoint MemPt, RetPt;
//MemPt.x = m_nClipOrgX + (point.x - m_nBorder);
//MemPt.y = m_nClipOrgY + (point.y - m_nBorder);
//RetPt.x = (MemPt.x - m_nMemSize / 2) / (m_nZoomRatio + m_nGrainGap);
//RetPt.y = (MemPt.y - m_nMemSize / 2) / (m_nZoomRatio + m_nGrainGap);
//RetPt.x = (MemPt.x - m_nMemSize / 2) >= 0 ? RetPt.x : RetPt.x - 1;
//RetPt.y = (MemPt.y - m_nMemSize / 2) >= 0 ? RetPt.y : RetPt.y - 1;
GetGrainCoordinate(point, RetPt);
m_pMainFrame = (CMainFrame*)AfxGetMainWnd();
::PostMessage(m_pMainFrame->GetSafeHwnd(), WM_RETGRAINCOORDINATE, RetPt.x, RetPt.y);
if(m_IsSelected[BATCH_SELECT])
{
m_SltStartPt = RetPt;
}
#ifdef _DEBUG
TRACE(_T("hit.x = %d, hit.y = %d\n"), RetPt.x, RetPt.y);
#endif
CWnd::OnLButtonDown(nFlags, point);
}
void CMapping2::OnLButtonUp(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
m_bLBtnIsDown = FALSE;
m_bIsDrag = FALSE;
ReleaseCapture();
Invalidate(FALSE);
CWnd::OnLButtonDown(nFlags, point);
}
void CMapping2::OnRButtonDown(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
//if(是否在Mapping上点击)
CRect rect;
GetClientRect(rect);
rect.left += m_nBorder;
rect.right -= m_nBorder;
rect.top += m_nBorder;
rect.bottom -= m_nBorder;
if(!rect.PtInRect(point))
{
// ReleaseCapture();
return;
}
HWND hParent = GetParentHWnd();
::PostMessage(hParent, WM_RIGHTHITMAPPING, point.x, point.y);
CWnd::OnRButtonDown(nFlags, point);
}
void CMapping2::OnMouseMove(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CRect rect;
GetClientRect(rect);
rect.left += m_nBorder;
rect.right -= m_nBorder;
rect.top += m_nBorder;
rect.bottom -= m_nBorder;
if(!rect.PtInRect(point))
{
return;
}
if(m_bLBtnIsDown)
{
int nValid = DRAG;
for(int i = 0; i != m_snMaxItem; i++)
{
if(m_IsSelected[i])
{
nValid = i;
break;
}
}
switch(nValid)
{
case DRAG:
{
m_bIsDrag = TRUE;
CPoint OffSet;
OffSet = point - m_StartPt;
m_nClipOrgX -= OffSet.x;
m_nClipOrgY -= OffSet.y;
if(m_nClipOrgX < 0)
m_nClipOrgX = 0;
else if(m_nClipOrgX > m_nMemSize - m_nWidth)
m_nClipOrgX = m_nMemSize - m_nWidth;
if(m_nClipOrgY < 0)
m_nClipOrgY = 0;
else if(m_nClipOrgY > m_nMemSize - m_nHeight)
m_nClipOrgY = m_nMemSize - m_nHeight;
m_StartPt = point;
// 发送 WM_PAINT
Invalidate(FALSE);
}
break;
case BATCH_SELECT:
{
// 发信息给父窗口,执行操作
// 恢复之前的颜色
// 重新画处目前的颜色
//DrawBatchGrain()
}
break;
default:
{
}
}
}
CPoint RetPt;
GetGrainCoordinate(point, RetPt);
m_pMainFrame = (CMainFrame*)AfxGetMainWnd();
::PostMessage(m_pMainFrame->m_hWnd, WM_MOUSEMOVEINMAPPING, RetPt.x, RetPt.y);
CWnd::OnMouseMove(nFlags, point);
}
BOOL CMapping2::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
// 发送 WM_PAINT
Invalidate(FALSE);
TRACE(_T("nFlags = %d, zDelta = %d, ptX = %d, ptY = %d\n"), nFlags, zDelta, pt.x, pt.y);
return CWnd::OnMouseWheel(nFlags, zDelta, pt);
}
void CMapping2::OnDestroy()
{
CWnd::OnDestroy();
// TODO: 在此处添加消息处理程序代码
m_MemDC.SelectObject(&m_pOldBitmap);
if(NULL != m_pDC)
{
ReleaseDC(m_pDC);
m_pDC = NULL;
}
m_MemDC.DeleteDC();
//m_pMemDC = NULL;
// top 位图环境没有恢复
m_TopMemDC.SelectObject(pTopOldFont);
m_TopMemDC.DeleteDC();
// Right 位图环境没有恢复
m_RightMemDC.SelectObject(pRightOldFont);
m_RightMemDC.DeleteDC();
}
int CMapping2::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CWnd::OnCreate(lpCreateStruct) == -1)
return -1;
// TODO: 在此添加您专用的创建代码
CRect rect;
GetClientRect(rect);
m_nWidth = rect.Width() - 2 * m_nBorder;
m_nHeight = rect.Height() - 2 * m_nBorder;
m_pDC = GetDC();
m_MemDC.CreateCompatibleDC(NULL);
m_bitmap.CreateCompatibleBitmap(m_pDC, m_nMemSize, m_nMemSize);
m_pOldBitmap = m_MemDC.SelectObject(&m_bitmap);
m_MemDC.FillSolidRect(0, 0, m_nMemSize, m_nMemSize, BACKGROUND_GRAY);
//////////////////////////////////////////////////////////////////////////
CFont font;
int nFontHeight = m_nZoomRatio + m_nGrainGap;
font.CreateFont(nFontHeight,
nFontHeight/3,
0,
0,
FW_THIN,
0,
0,
0,
DEFAULT_CHARSET,
OUT_CHARACTER_PRECIS,
CLIP_CHARACTER_PRECIS,
DEFAULT_QUALITY,
DEFAULT_PITCH | FF_DONTCARE,
/* _T("Arial")*/_T("Courier New"));
// Top
m_TopMemDC.CreateCompatibleDC(NULL);
m_Topbitmap.CreateCompatibleBitmap(m_pDC, m_nMemSize, m_nBorder);
m_TopMemDC.SelectObject(&m_Topbitmap);
pTopOldFont = m_TopMemDC.SelectObject(&font);
m_TopMemDC.FillSolidRect(0, 0, m_nMemSize, m_nBorder, LEGEND_BACKGROUND);
//Right
m_RightMemDC.CreateCompatibleDC(NULL);
m_Rightbitmap.CreateCompatibleBitmap(m_pDC, m_nBorder, m_nMemSize);
m_RightMemDC.SelectObject(&m_Rightbitmap);
pRightOldFont = m_RightMemDC.SelectObject(&font);
m_RightMemDC.FillSolidRect(0, 0, m_nBorder, m_nMemSize, LEGEND_BACKGROUND);
return 0;
}
int CMapping2::SetGrainZoomRatio(int nZoomRatio)
{
if(nZoomRatio < 1 || nZoomRatio > m_nMaxZoom)
return FALSE;
m_nZoomRatio = nZoomRatio;
return TRUE;
}
int CMapping2::GetGrainZoomRatio()
{
return m_nZoomRatio;
}
int CMapping2::SetWaferSize(int nCols, int nRows)
{
if(0 == nCols || 0 == nRows)
return FALSE;
m_nDieCols = nCols;
m_nDieRows = nRows;
CRect rect;
GetClientRect(rect);
m_nMaxZoom = (int)min((m_nMemSize - m_nBorder) / m_nDieCols, (m_nMemSize - m_nBorder) / m_nDieRows);
m_nMaxZoom -= m_nGrainGap;
if(m_nZoomRatio > m_nMaxZoom)
m_nZoomRatio = m_nMaxZoom;
if (m_nZoomRatio <= 1)
m_nZoomRatio = 1;
return TRUE;
}
CPoint CMapping2::getWaferSize()
{
CPoint pt;
pt.x = m_nDieCols;
pt.y = m_nDieRows;
return pt;
}
int CMapping2::DrawSingleGrain(int nCols, int nRows, COLORREF clr, BOOL bIsRefresh)
{
int nx = nCols*(m_nZoomRatio + m_nGrainGap) + m_nMemSize / 2;
int ny = nRows*(m_nZoomRatio + m_nGrainGap) + m_nMemSize / 2;
m_MemDC.FillSolidRect(nx, ny, m_nZoomRatio, m_nZoomRatio, clr);
/*CRect rect(m_nClipOrgX, m_nClipOrgY, m_nClipOrgX + m_nWidth, m_nClipOrgY + m_nHeight);
if(!rect.PtInRect(CPoint(x, y)))
{
if(x)
}
*/
if(!m_bIsDrag)
{
// 超出裁剪区域时,主动改变裁剪坐标
if(nx > m_nClipOrgX + m_nWidth)
m_nClipOrgX = min(m_nMemSize - m_nWidth, m_nClipOrgX + m_nJump * m_nZoomRatio);
if(nx < m_nClipOrgX)
m_nClipOrgX = max(0, m_nClipOrgX - m_nJump * m_nZoomRatio);
if(ny > m_nClipOrgY + m_nHeight)
m_nClipOrgY = min(m_nMemSize - m_nHeight, m_nClipOrgY + m_nJump * m_nZoomRatio);
if(ny < m_nClipOrgY)
m_nClipOrgY = max(0, m_nClipOrgY - m_nJump * m_nZoomRatio);
}
if(bIsRefresh)
{
Invalidate(FALSE); // 不擦除背景,直接画
}
return TRUE;
}
int CMapping2::DrawBatchGrain(CPoint Start, CPoint End, COLORREF clr)
{
int nStartX = 0, nEndX = 0;
int nStartY = 0, nEndY = 0;
nStartX = Start.x;
nStartY = Start.y;
nEndX = End.x;
nEndY = End.y;
// 特殊情况考虑 如: 起始点 = 右下角 结束点 = 左上角
if(nStartX >= nEndX)
swap(nStartX, nEndX);
if(nStartY >= nEndY)
swap(nStartY, nEndY);
for(int i = nStartY; i != nEndY; i++)
{
for(int j = nStartX; j != nEndX; j++)
{
DrawSingleGrain(j, i, SELECT_COLOR);
}
}
return TRUE;
}
int CMapping2::ClearSingleGrain(int nCols, int nRows/*, COLORREF clr*/)
{
if (0 >= nCols ||0 >= nRows)
return FALSE;
DrawSingleGrain(nCols, nRows, BACKGROUND_GRAY);
return TRUE;
}
int CMapping2::ClearAll()
{
// Mapping 背景色
m_MemDC.FillSolidRect(0, 0, m_nMemSize, m_nMemSize, BACKGROUND_GRAY);
Invalidate(FALSE);
return TRUE;
}
int CMapping2::UpdateMapping()
{
Invalidate(FALSE);
return TRUE;
}
int CMapping2::SetLegend(int nCol, int nRow)
{
if (0 >= nCol || 0 >= nRow)
{
return FALSE;
}
m_TopMemDC.FillSolidRect(0, 0, m_nMemSize, m_nBorder, LEGEND_BACKGROUND);
m_RightMemDC.FillSolidRect(0, 0, m_nBorder, m_nMemSize, LEGEND_BACKGROUND);
BOOL bIsOdd = nCol%2;
for(int i = -nCol/2; i <= nCol/2; i++)
{
if ((!bIsOdd) && i >= 0)
{
if(1 <= i)
{
CString str;
str.Format(_T("%d"), i);
DrawLegendX(i - 1, str);
}
}
else
{
CString str;
str.Format(_T("%d"), i);
DrawLegendX(i, str);
}
}
bIsOdd = nRow%2;
for(int i = -nRow/2; i <= nRow/2; i++)
{
if ((!bIsOdd) && i >= 0)
{
if(1 <= i)
{
CString str;
str.Format(_T("%d"), i);
DrawLegendY(i - 1, str);
}
}
else
{
CString str;
str.Format(_T("%d"), i);
DrawLegendY(i, str);
}
}
Invalidate(FALSE);
return TRUE;
}
int CMapping2::SetFunctionFlag(int nIndex)
{
if(nIndex < 0 || nIndex >= m_snMaxItem)
return FALSE;
for(int i = 0; i != m_snMaxItem; i++)
{
m_IsSelected[i] = (i == nIndex) ? TRUE : FALSE;
}
return TRUE;
}
int CMapping2::DrawLegendX(int nCol, CString str)
{
if(_T("") == str)
return FALSE;
int nx = nCol*(m_nZoomRatio + m_nGrainGap) + m_nMemSize / 2;
int ny = 0;
m_TopMemDC.TextOut(nx, ny, str);
return TRUE;
}
int CMapping2::DrawLegendY(int nRow, CString str)
{
if(_T("") == str)
return FALSE;
int nx = 3;
int ny = nRow*(m_nZoomRatio + m_nGrainGap) + m_nMemSize / 2;
m_RightMemDC.TextOut(nx, ny, str);
return TRUE;
}
int CMapping2::SaveMapping(LPCTSTR lpszPath, BOOL bIsClip)
{
CImage image;
int nBoder = 10;
if(bIsClip)
{
int nWidth = m_nDieCols*(m_nZoomRatio + m_nGrainGap);
int nHeight = m_nDieRows*(m_nZoomRatio + m_nGrainGap);
image.Create(nWidth , nHeight , 32);
::BitBlt(image.GetDC() ,0 ,0 ,nWidth ,nHeight ,m_MemDC.m_hDC ,(m_nMemSize- nWidth)/2 - nBoder, (m_nMemSize- nHeight)/2 - nBoder, SRCCOPY);
}
else
{
HBITMAP hBitMap = HBITMAP(m_bitmap);
CImage image;
image.Attach(hBitMap);
}
MakeSurePathExist(lpszPath);
HRESULT hr = image.Save(lpszPath);
return (hr == S_OK);
}
int CMapping2::LoadMapping(LPCTSTR lpszPath)
{
return TRUE;
}
HWND CMapping2::GetParentHWnd()
{
m_pMainFrame = (CMainFrame*)AfxGetMainWnd();
return m_pMainFrame->m_wndBottomBar.m_hWnd;
}
int CMapping2::MakeSurePathExist(CString strPath)
{
int nSize = strPath.GetLength() * 2 + 1;
wchar_t *pWchar_t = strPath.GetBuffer();
char *pChar = new char[nSize];
if(NULL == pChar)
return FALSE;
memset(pChar, 0, (size_t)nSize);
wcstombs(pChar, pWchar_t, nSize);
if (!MakeSureDirectoryPathExists(pChar))
return FALSE;
delete []pChar;
return TRUE;
}
int CMapping2::GetGrainCoordinate(const CPoint PixelCoordinates, CPoint& GrainCoordinates)
{
CPoint MemPt, RetPt;
MemPt.x = m_nClipOrgX + (PixelCoordinates.x - m_nBorder);
MemPt.y = m_nClipOrgY + (PixelCoordinates.y - m_nBorder);
RetPt.x = (MemPt.x - m_nMemSize / 2) / (m_nZoomRatio + m_nGrainGap);
RetPt.y = (MemPt.y - m_nMemSize / 2) / (m_nZoomRatio + m_nGrainGap);
RetPt.x = (MemPt.x - m_nMemSize / 2) >= 0 ? RetPt.x : RetPt.x - 1;
RetPt.y = (MemPt.y - m_nMemSize / 2) >= 0 ? RetPt.y : RetPt.y - 1;
GrainCoordinates = RetPt;
return TRUE;
}
看起来程序比较多,但大部分是增加功能的,对本文讲述没有影响,我给出三点总结:
1. 模仿MFC自带的控件,如:按钮CButton, 继承CWnd类。
2. 如果要在控件中绘图的话,一定要在onPaint()消息响应中,因为在标准客户区绘图,窗口刷新时,图案不消失,在 WM_PAINT下回调onPaint().
3. 我们开发的控件,也只能用动态方式创建:使用方式如下:
CRect rect;
CWnd *pWnd = GetDlgItem(IDC_STATIC_MAPPING);
pWnd->GetClientRect(rect);
m_Mapping.Create(NULL, NULL, WS_CHILD|WS_VISIBLE|SS_CENTER, rect, pWnd, 100081);
IDC_STATIC_MAPPING是一个静态文本控件,主要是用来设置开发控件的尺寸大小。
完成上面三个主要的步骤,就可以绘图了,下面是我的demo演示:
使用代码演示:
m_Mapping.ClearAll();
int nCol = 50, nRow = 51;
int nColGap = 2+5; int nRowGap = 2+5;
int nR = 175;
for(int i = 0; i != nRow/2; i++)
{
if(!(nCol%2))
{
int L = sqrt(double(nR*nR - (i*1)*nRowGap*(i*1)*nRowGap));
int N = (L - 6)/nColGap;
for(int j = 0; j != N; j++)
{
m_Mapping.DrawSingleGrain(j, i+1, RGB(rand()%255, rand()%255, rand()%255));
m_Mapping.DrawSingleGrain(-(j+1), i+1, RGB(rand()%255, rand()%255, rand()%255));
m_Mapping.DrawSingleGrain(j, -(i+1), RGB(rand()%255, rand()%255, rand()%255));
m_Mapping.DrawSingleGrain(-(j+1), -(i+1), RGB(rand()%255, rand()%255, rand()%255));
}
}
}
for(int i = 0; i != nCol/2; i++)
{
m_Mapping.DrawSingleGrain(i, 0, RGB(rand()%255, rand()%255, rand()%255));
m_Mapping.DrawSingleGrain(-i, 0, RGB(rand()%255, rand()%255, rand()%255));
}
窗口演示效果:
本文是开发新控件的一种方式,还有一种是开发成 “ActiceX” 格式的,比如 tree Chart绘图控件,就是这样式儿的,不过后者相对前者比较繁杂,一般开发功能性不是很复杂的控件,用本文介绍的方式,周期短,性价高。
好了,以上就是我开发控件的流程,时间仓促,文中难免有什么错误之处,还请大家谅解。
来源:CSDN
作者:英语饲养员
链接:https://blog.csdn.net/hello071375/article/details/104648499